diff --git a/composer.json b/composer.json index 42ee51182..173ea9b9e 100644 --- a/composer.json +++ b/composer.json @@ -45,18 +45,19 @@ "psr/clock": "^1.0", "psr/container": "^2.0.2", "psr/event-dispatcher": "^1.0", + "psr/log": "^3.0.2", "punic/punic": "^3.8", "sabre/dav": "^4.4.0", "scssphp/scssphp": "^1.12", - "stecman/symfony-console-completion": "^0.11.0", - "symfony/console": "^5.4.24", - "symfony/event-dispatcher": "^5.4.26", - "symfony/http-foundation": "^5.4.24", - "symfony/mailer": "^5.4.22", - "symfony/polyfill-intl-grapheme": "^1.28", - "symfony/polyfill-intl-normalizer": "^1.28", - "symfony/process": "^5.4.34", - "symfony/routing": "^5.4.24", + "stecman/symfony-console-completion": "^0.13.0", + "symfony/console": "^6.4.12", + "symfony/event-dispatcher": "^6.4.8", + "symfony/http-foundation": "^6.4.12", + "symfony/mailer": "^6.4", + "symfony/polyfill-intl-grapheme": "^1.31.0", + "symfony/polyfill-intl-normalizer": "^1.31.0", + "symfony/process": "^6.4.12", + "symfony/routing": "^6.4.12", "symfony/translation": "^6.4.4", "wapmorgan/mp3info": "^0.1.0", "web-auth/webauthn-lib": "^4.9.1" diff --git a/composer.lock b/composer.lock index b17151215..248174622 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "67a25f956192f27cc89bcab84bfdb7c6", + "content-hash": "b9af8f022789889a84b87cb841809891", "packages": [ { "name": "aws/aws-crt-php", @@ -3299,16 +3299,16 @@ }, { "name": "psr/log", - "version": "2.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", - "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", + "url": "https://github.com/gitapi/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -3317,7 +3317,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { @@ -3343,9 +3343,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/2.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:41:46+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "punic/punic", @@ -4199,29 +4199,30 @@ }, { "name": "stecman/symfony-console-completion", - "version": "0.11.0", + "version": "v0.13.0", "source": { "type": "git", "url": "https://github.com/stecman/symfony-console-completion.git", - "reference": "a9502dab59405e275a9f264536c4e1cb61fc3518" + "reference": "769e6b49123847648ae80f10d3ccb0f128cbb953" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/stecman/symfony-console-completion/zipball/a9502dab59405e275a9f264536c4e1cb61fc3518", - "reference": "a9502dab59405e275a9f264536c4e1cb61fc3518", + "url": "https://github.com/gitapi/repos/stecman/symfony-console-completion/zipball/769e6b49123847648ae80f10d3ccb0f128cbb953", + "reference": "769e6b49123847648ae80f10d3ccb0f128cbb953", "shasum": "" }, "require": { - "php": ">=5.3.2", - "symfony/console": "~2.3 || ~3.0 || ~4.0 || ~5.0" + "php": ">=8.0.2", + "symfony/console": "~6.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8.36 || ~5.7 || ~6.4" + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.10.x-dev" + "dev-master": "0.14.x-dev" } }, "autoload": { @@ -4242,58 +4243,53 @@ "description": "Automatic BASH completion for Symfony Console Component based applications.", "support": { "issues": "https://github.com/stecman/symfony-console-completion/issues", - "source": "https://github.com/stecman/symfony-console-completion/tree/0.11.0" + "source": "https://github.com/stecman/symfony-console-completion/tree/v0.13.0" }, - "time": "2019-11-24T17:03:06+00:00" + "time": "2024-07-05T11:49:57+00:00" }, { "name": "symfony/console", - "version": "v5.4.35", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931" + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931", + "url": "https://github.com/gitapi/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" }, "provide": { - "psr/log-implementation": "1.0|2.0" + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4327,7 +4323,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.35" + "source": "https://github.com/symfony/console/tree/v6.4.12" }, "funding": [ { @@ -4343,7 +4339,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:28:09+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/css-selector", @@ -4546,44 +4542,39 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.4.26", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac" + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/event-dispatcher/zipball/5dcc00e03413f05c1e7900090927bb7247cb0aac", - "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac", + "url": "https://github.com/gitapi/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4611,7 +4602,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.26" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" }, "funding": [ { @@ -4627,33 +4618,30 @@ "type": "tidelift" } ], - "time": "2023-07-06T06:34:20+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.0.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/event-dispatcher-contracts/zipball/7bc61cc2db649b4637d331240c5346dcc7708051", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051", + "url": "https://github.com/gitapi/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -4690,7 +4678,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -4706,39 +4694,40 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.4.25", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b" + "reference": "133ac043875f59c26c55e79cf074562127cce4d2" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/http-foundation/zipball/f66be2706075c5f6325d2fe2b743a57fb5d23f6b", - "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b", + "url": "https://github.com/gitapi/repos/symfony/http-foundation/zipball/133ac043875f59c26c55e79cf074562127cce4d2", + "reference": "133ac043875f59c26c55e79cf074562127cce4d2", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-php83": "^1.27" }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" + "conflict": { + "symfony/cache": "<6.3" }, - "suggest": { - "symfony/mime": "To use the file extension guesser" + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4766,7 +4755,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.25" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.12" }, "funding": [ { @@ -4782,39 +4771,43 @@ "type": "tidelift" } ], - "time": "2023-06-22T08:06:06+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/mailer", - "version": "v5.4.22", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b" + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/mailer/zipball/6330cd465dfd8b7a07515757a1c37069075f7b0b", - "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b", + "url": "https://github.com/gitapi/repos/symfony/mailer/zipball/b6a25408c569ae2366b3f663a4edad19420a9c26", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/mime": "^5.2.6|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/http-kernel": "<4.4" + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" }, "require-dev": { - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" }, "type": "library", "autoload": { @@ -4842,7 +4835,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v5.4.22" + "source": "https://github.com/symfony/mailer/tree/v6.4.12" }, "funding": [ { @@ -4858,43 +4851,44 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:15:32+00:00" + "time": "2024-09-08T12:30:05+00:00" }, { "name": "symfony/mime", - "version": "v5.4.19", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a858429a9c704edc53fe057228cf9ca282ba48eb" + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/mime/zipball/a858429a9c704edc53fe057228cf9ca282ba48eb", - "reference": "a858429a9c704edc53fe057228cf9ca282ba48eb", + "url": "https://github.com/gitapi/repos/symfony/mime/zipball/abe16ee7790b16aa525877419deb0f113953f0e1", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<4.4", - "symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6" + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/property-access": "^4.4|^5.1|^6.0", - "symfony/property-info": "^4.4|^5.1|^6.0", - "symfony/serializer": "^5.4.14|~6.0.14|^6.1.6" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -4926,7 +4920,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.19" + "source": "https://github.com/symfony/mime/tree/v6.4.12" }, "funding": [ { @@ -4942,7 +4936,7 @@ "type": "tidelift" } ], - "time": "2023-01-09T05:43:46+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5103,22 +5097,21 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" @@ -5167,7 +5160,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -5183,7 +5176,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -5347,90 +5340,17 @@ "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php72", + "name": "symfony/polyfill-php80", "version": "v1.29.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" - }, - "dist": { - "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-29T20:11:03+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "url": "https://github.com/gitapi/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -5438,9 +5358,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5451,7 +5368,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, "classmap": [ "Resources/stubs" @@ -5462,6 +5379,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -5471,7 +5392,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -5480,7 +5401,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -5496,24 +5417,24 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "name": "symfony/polyfill-php83", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://github.com/gitapi/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -5527,7 +5448,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -5538,10 +5459,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -5551,7 +5468,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -5560,7 +5477,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -5576,7 +5493,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-uuid", @@ -5659,21 +5576,20 @@ }, { "name": "symfony/process", - "version": "v5.4.34", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a" + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/process/zipball/8fa22178dfc368911dbd513b431cd9b06f9afe7a", - "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a", + "url": "https://github.com/gitapi/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3", + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -5701,7 +5617,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.34" + "source": "https://github.com/symfony/process/tree/v6.4.12" }, "funding": [ { @@ -5717,47 +5633,40 @@ "type": "tidelift" } ], - "time": "2023-12-02T08:41:43+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "symfony/routing", - "version": "v5.4.25", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649" + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/routing/zipball/56bfc1394f7011303eb2e22724f9b422d3f14649", - "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649", + "url": "https://github.com/gitapi/repos/symfony/routing/zipball/a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.12", - "symfony/config": "<5.3", - "symfony/dependency-injection": "<4.4", - "symfony/yaml": "<4.4" + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^5.3|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "For using the all-in-one router or any loader", - "symfony/expression-language": "For using expression matching", - "symfony/http-foundation": "For using a Symfony Request object", - "symfony/yaml": "For using the YAML loader" + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -5791,7 +5700,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v5.4.25" + "source": "https://github.com/symfony/routing/tree/v6.4.12" }, "funding": [ { @@ -5807,36 +5716,34 @@ "type": "tidelift" } ], - "time": "2023-06-05T14:18:47+00:00" + "time": "2024-09-20T08:32:26+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.0.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "url": "https://github.com/gitapi/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { - "php": ">=8.0.2", - "psr/container": "^2.0" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5846,7 +5753,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5873,7 +5783,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -5889,7 +5799,7 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:58+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index 8cfb6d0c1..15e8da833 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -1211,6 +1211,15 @@ 'Cose\\Key\\OkpKey' => $vendorDir . '/web-auth/cose-lib/src/Key/OkpKey.php', 'Cose\\Key\\RsaKey' => $vendorDir . '/web-auth/cose-lib/src/Key/RsaKey.php', 'Cose\\Key\\SymmetricKey' => $vendorDir . '/web-auth/cose-lib/src/Key/SymmetricKey.php', + 'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php', + 'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php', + 'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php', + 'DateInvalidTimeZoneException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php', + 'DateMalformedIntervalStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php', + 'DateMalformedPeriodStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php', + 'DateMalformedStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php', + 'DateObjectError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php', + 'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php', 'Doctrine\\Common\\Cache\\Cache' => $vendorDir . '/doctrine/cache/lib/Doctrine/Common/Cache/Cache.php', 'Doctrine\\Common\\Cache\\CacheProvider' => $vendorDir . '/doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php', 'Doctrine\\Common\\Cache\\ClearableCache' => $vendorDir . '/doctrine/cache/lib/Doctrine/Common/Cache/ClearableCache.php', @@ -1813,7 +1822,6 @@ 'JmesPath\\TreeCompiler' => $vendorDir . '/mtdowling/jmespath.php/src/TreeCompiler.php', 'JmesPath\\TreeInterpreter' => $vendorDir . '/mtdowling/jmespath.php/src/TreeInterpreter.php', 'JmesPath\\Utils' => $vendorDir . '/mtdowling/jmespath.php/src/Utils.php', - 'JsonException' => $vendorDir . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', 'JsonSchema\\Constraints\\BaseConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php', 'JsonSchema\\Constraints\\CollectionConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php', 'JsonSchema\\Constraints\\Constraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php', @@ -2161,6 +2169,7 @@ 'OpenStack\\ObjectStore\\v1\\Params' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Params.php', 'OpenStack\\ObjectStore\\v1\\Service' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Service.php', 'OpenStack\\OpenStack' => $vendorDir . '/php-opencloud/openstack/src/OpenStack.php', + 'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php', 'PEAR' => $vendorDir . '/pear/pear-core-minimal/src/PEAR.php', 'PEAR_Error' => $vendorDir . '/pear/pear-core-minimal/src/PEAR.php', 'PEAR_ErrorStack' => $vendorDir . '/pear/pear-core-minimal/src/PEAR/ErrorStack.php', @@ -2245,6 +2254,7 @@ 'Punic\\Script' => $vendorDir . '/punic/punic/src/Script.php', 'Punic\\Territory' => $vendorDir . '/punic/punic/src/Territory.php', 'Punic\\Unit' => $vendorDir . '/punic/punic/src/Unit.php', + 'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php', 'Sabre\\CalDAV\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php', 'Sabre\\CalDAV\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php', 'Sabre\\CalDAV\\Backend\\NotificationSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php', @@ -2986,19 +2996,25 @@ 'Symfony\\Component\\Console\\Command\\ListCommand' => $vendorDir . '/symfony/console/Command/ListCommand.php', 'Symfony\\Component\\Console\\Command\\LockableTrait' => $vendorDir . '/symfony/console/Command/LockableTrait.php', 'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => $vendorDir . '/symfony/console/Command/SignalableCommandInterface.php', + 'Symfony\\Component\\Console\\Command\\TraceableCommand' => $vendorDir . '/symfony/console/Command/TraceableCommand.php', 'Symfony\\Component\\Console\\Completion\\CompletionInput' => $vendorDir . '/symfony/console/Completion/CompletionInput.php', 'Symfony\\Component\\Console\\Completion\\CompletionSuggestions' => $vendorDir . '/symfony/console/Completion/CompletionSuggestions.php', 'Symfony\\Component\\Console\\Completion\\Output\\BashCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/BashCompletionOutput.php', 'Symfony\\Component\\Console\\Completion\\Output\\CompletionOutputInterface' => $vendorDir . '/symfony/console/Completion/Output/CompletionOutputInterface.php', + 'Symfony\\Component\\Console\\Completion\\Output\\FishCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/FishCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Output\\ZshCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/ZshCompletionOutput.php', 'Symfony\\Component\\Console\\Completion\\Suggestion' => $vendorDir . '/symfony/console/Completion/Suggestion.php', 'Symfony\\Component\\Console\\ConsoleEvents' => $vendorDir . '/symfony/console/ConsoleEvents.php', 'Symfony\\Component\\Console\\Cursor' => $vendorDir . '/symfony/console/Cursor.php', + 'Symfony\\Component\\Console\\DataCollector\\CommandDataCollector' => $vendorDir . '/symfony/console/DataCollector/CommandDataCollector.php', + 'Symfony\\Component\\Console\\Debug\\CliRequest' => $vendorDir . '/symfony/console/Debug/CliRequest.php', 'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => $vendorDir . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php', 'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => $vendorDir . '/symfony/console/Descriptor/ApplicationDescription.php', 'Symfony\\Component\\Console\\Descriptor\\Descriptor' => $vendorDir . '/symfony/console/Descriptor/Descriptor.php', 'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => $vendorDir . '/symfony/console/Descriptor/DescriptorInterface.php', 'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => $vendorDir . '/symfony/console/Descriptor/JsonDescriptor.php', 'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => $vendorDir . '/symfony/console/Descriptor/MarkdownDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\ReStructuredTextDescriptor' => $vendorDir . '/symfony/console/Descriptor/ReStructuredTextDescriptor.php', 'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => $vendorDir . '/symfony/console/Descriptor/TextDescriptor.php', 'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => $vendorDir . '/symfony/console/Descriptor/XmlDescriptor.php', 'Symfony\\Component\\Console\\EventListener\\ErrorListener' => $vendorDir . '/symfony/console/EventListener/ErrorListener.php', @@ -3014,6 +3030,7 @@ 'Symfony\\Component\\Console\\Exception\\LogicException' => $vendorDir . '/symfony/console/Exception/LogicException.php', 'Symfony\\Component\\Console\\Exception\\MissingInputException' => $vendorDir . '/symfony/console/Exception/MissingInputException.php', 'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => $vendorDir . '/symfony/console/Exception/NamespaceNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\RunCommandFailedException' => $vendorDir . '/symfony/console/Exception/RunCommandFailedException.php', 'Symfony\\Component\\Console\\Exception\\RuntimeException' => $vendorDir . '/symfony/console/Exception/RuntimeException.php', 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatter.php', 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatterStyle.php', @@ -3031,6 +3048,7 @@ 'Symfony\\Component\\Console\\Helper\\HelperInterface' => $vendorDir . '/symfony/console/Helper/HelperInterface.php', 'Symfony\\Component\\Console\\Helper\\HelperSet' => $vendorDir . '/symfony/console/Helper/HelperSet.php', 'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => $vendorDir . '/symfony/console/Helper/InputAwareHelper.php', + 'Symfony\\Component\\Console\\Helper\\OutputWrapper' => $vendorDir . '/symfony/console/Helper/OutputWrapper.php', 'Symfony\\Component\\Console\\Helper\\ProcessHelper' => $vendorDir . '/symfony/console/Helper/ProcessHelper.php', 'Symfony\\Component\\Console\\Helper\\ProgressBar' => $vendorDir . '/symfony/console/Helper/ProgressBar.php', 'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => $vendorDir . '/symfony/console/Helper/ProgressIndicator.php', @@ -3053,6 +3071,10 @@ 'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => $vendorDir . '/symfony/console/Input/StreamableInputInterface.php', 'Symfony\\Component\\Console\\Input\\StringInput' => $vendorDir . '/symfony/console/Input/StringInput.php', 'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => $vendorDir . '/symfony/console/Logger/ConsoleLogger.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandContext' => $vendorDir . '/symfony/console/Messenger/RunCommandContext.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessage' => $vendorDir . '/symfony/console/Messenger/RunCommandMessage.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessageHandler' => $vendorDir . '/symfony/console/Messenger/RunCommandMessageHandler.php', + 'Symfony\\Component\\Console\\Output\\AnsiColorMode' => $vendorDir . '/symfony/console/Output/AnsiColorMode.php', 'Symfony\\Component\\Console\\Output\\BufferedOutput' => $vendorDir . '/symfony/console/Output/BufferedOutput.php', 'Symfony\\Component\\Console\\Output\\ConsoleOutput' => $vendorDir . '/symfony/console/Output/ConsoleOutput.php', 'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => $vendorDir . '/symfony/console/Output/ConsoleOutputInterface.php', @@ -3065,6 +3087,7 @@ 'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => $vendorDir . '/symfony/console/Question/ChoiceQuestion.php', 'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => $vendorDir . '/symfony/console/Question/ConfirmationQuestion.php', 'Symfony\\Component\\Console\\Question\\Question' => $vendorDir . '/symfony/console/Question/Question.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalMap' => $vendorDir . '/symfony/console/SignalRegistry/SignalMap.php', 'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => $vendorDir . '/symfony/console/SignalRegistry/SignalRegistry.php', 'Symfony\\Component\\Console\\SingleCommandApplication' => $vendorDir . '/symfony/console/SingleCommandApplication.php', 'Symfony\\Component\\Console\\Style\\OutputStyle' => $vendorDir . '/symfony/console/Style/OutputStyle.php', @@ -3146,10 +3169,10 @@ 'Symfony\\Component\\EventDispatcher\\EventSubscriberInterface' => $vendorDir . '/symfony/event-dispatcher/EventSubscriberInterface.php', 'Symfony\\Component\\EventDispatcher\\GenericEvent' => $vendorDir . '/symfony/event-dispatcher/GenericEvent.php', 'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => $vendorDir . '/symfony/event-dispatcher/ImmutableEventDispatcher.php', - 'Symfony\\Component\\EventDispatcher\\LegacyEventDispatcherProxy' => $vendorDir . '/symfony/event-dispatcher/LegacyEventDispatcherProxy.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => $vendorDir . '/symfony/http-foundation/AcceptHeader.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => $vendorDir . '/symfony/http-foundation/AcceptHeaderItem.php', 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => $vendorDir . '/symfony/http-foundation/BinaryFileResponse.php', + 'Symfony\\Component\\HttpFoundation\\ChainRequestMatcher' => $vendorDir . '/symfony/http-foundation/ChainRequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\Cookie' => $vendorDir . '/symfony/http-foundation/Cookie.php', 'Symfony\\Component\\HttpFoundation\\Exception\\BadRequestException' => $vendorDir . '/symfony/http-foundation/Exception/BadRequestException.php', 'Symfony\\Component\\HttpFoundation\\Exception\\ConflictingHeadersException' => $vendorDir . '/symfony/http-foundation/Exception/ConflictingHeadersException.php', @@ -3157,6 +3180,7 @@ 'Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface' => $vendorDir . '/symfony/http-foundation/Exception/RequestExceptionInterface.php', 'Symfony\\Component\\HttpFoundation\\Exception\\SessionNotFoundException' => $vendorDir . '/symfony/http-foundation/Exception/SessionNotFoundException.php', 'Symfony\\Component\\HttpFoundation\\Exception\\SuspiciousOperationException' => $vendorDir . '/symfony/http-foundation/Exception/SuspiciousOperationException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\UnexpectedValueException' => $vendorDir . '/symfony/http-foundation/Exception/UnexpectedValueException.php', 'Symfony\\Component\\HttpFoundation\\ExpressionRequestMatcher' => $vendorDir . '/symfony/http-foundation/ExpressionRequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\FileBag' => $vendorDir . '/symfony/http-foundation/FileBag.php', 'Symfony\\Component\\HttpFoundation\\File\\Exception\\AccessDeniedException' => $vendorDir . '/symfony/http-foundation/File/Exception/AccessDeniedException.php', @@ -3181,18 +3205,28 @@ 'Symfony\\Component\\HttpFoundation\\JsonResponse' => $vendorDir . '/symfony/http-foundation/JsonResponse.php', 'Symfony\\Component\\HttpFoundation\\ParameterBag' => $vendorDir . '/symfony/http-foundation/ParameterBag.php', 'Symfony\\Component\\HttpFoundation\\RateLimiter\\AbstractRequestRateLimiter' => $vendorDir . '/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\PeekableRequestRateLimiterInterface' => $vendorDir . '/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php', 'Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface' => $vendorDir . '/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php', 'Symfony\\Component\\HttpFoundation\\RedirectResponse' => $vendorDir . '/symfony/http-foundation/RedirectResponse.php', 'Symfony\\Component\\HttpFoundation\\Request' => $vendorDir . '/symfony/http-foundation/Request.php', 'Symfony\\Component\\HttpFoundation\\RequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\RequestMatcherInterface' => $vendorDir . '/symfony/http-foundation/RequestMatcherInterface.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\RequestStack' => $vendorDir . '/symfony/http-foundation/RequestStack.php', 'Symfony\\Component\\HttpFoundation\\Response' => $vendorDir . '/symfony/http-foundation/Response.php', 'Symfony\\Component\\HttpFoundation\\ResponseHeaderBag' => $vendorDir . '/symfony/http-foundation/ResponseHeaderBag.php', 'Symfony\\Component\\HttpFoundation\\ServerBag' => $vendorDir . '/symfony/http-foundation/ServerBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag' => $vendorDir . '/symfony/http-foundation/Session/Attribute/AttributeBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface' => $vendorDir . '/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php', - 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag' => $vendorDir . '/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\FlashBagAwareSessionInterface' => $vendorDir . '/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php', 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag' => $vendorDir . '/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag' => $vendorDir . '/symfony/http-foundation/Session/Flash/FlashBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface' => $vendorDir . '/symfony/http-foundation/Session/Flash/FlashBagInterface.php', @@ -3225,19 +3259,35 @@ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy' => $vendorDir . '/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy' => $vendorDir . '/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php', - 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\ServiceSessionFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php', + 'Symfony\\Component\\HttpFoundation\\StreamedJsonResponse' => $vendorDir . '/symfony/http-foundation/StreamedJsonResponse.php', 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => $vendorDir . '/symfony/http-foundation/StreamedResponse.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\RequestAttributeValueSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseCookieValueSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseFormatSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasCookie' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasHeader' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderLocationSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsRedirected' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsSuccessful' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsUnprocessable' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseStatusCodeSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php', + 'Symfony\\Component\\HttpFoundation\\UriSigner' => $vendorDir . '/symfony/http-foundation/UriSigner.php', 'Symfony\\Component\\HttpFoundation\\UrlHelper' => $vendorDir . '/symfony/http-foundation/UrlHelper.php', + 'Symfony\\Component\\Mailer\\Command\\MailerTestCommand' => $vendorDir . '/symfony/mailer/Command/MailerTestCommand.php', 'Symfony\\Component\\Mailer\\DataCollector\\MessageDataCollector' => $vendorDir . '/symfony/mailer/DataCollector/MessageDataCollector.php', 'Symfony\\Component\\Mailer\\DelayedEnvelope' => $vendorDir . '/symfony/mailer/DelayedEnvelope.php', 'Symfony\\Component\\Mailer\\Envelope' => $vendorDir . '/symfony/mailer/Envelope.php', 'Symfony\\Component\\Mailer\\EventListener\\EnvelopeListener' => $vendorDir . '/symfony/mailer/EventListener/EnvelopeListener.php', 'Symfony\\Component\\Mailer\\EventListener\\MessageListener' => $vendorDir . '/symfony/mailer/EventListener/MessageListener.php', 'Symfony\\Component\\Mailer\\EventListener\\MessageLoggerListener' => $vendorDir . '/symfony/mailer/EventListener/MessageLoggerListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessengerTransportListener' => $vendorDir . '/symfony/mailer/EventListener/MessengerTransportListener.php', + 'Symfony\\Component\\Mailer\\Event\\FailedMessageEvent' => $vendorDir . '/symfony/mailer/Event/FailedMessageEvent.php', 'Symfony\\Component\\Mailer\\Event\\MessageEvent' => $vendorDir . '/symfony/mailer/Event/MessageEvent.php', 'Symfony\\Component\\Mailer\\Event\\MessageEvents' => $vendorDir . '/symfony/mailer/Event/MessageEvents.php', + 'Symfony\\Component\\Mailer\\Event\\SentMessageEvent' => $vendorDir . '/symfony/mailer/Event/SentMessageEvent.php', 'Symfony\\Component\\Mailer\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/mailer/Exception/ExceptionInterface.php', 'Symfony\\Component\\Mailer\\Exception\\HttpTransportException' => $vendorDir . '/symfony/mailer/Exception/HttpTransportException.php', 'Symfony\\Component\\Mailer\\Exception\\IncompleteDsnException' => $vendorDir . '/symfony/mailer/Exception/IncompleteDsnException.php', @@ -3246,6 +3296,7 @@ 'Symfony\\Component\\Mailer\\Exception\\RuntimeException' => $vendorDir . '/symfony/mailer/Exception/RuntimeException.php', 'Symfony\\Component\\Mailer\\Exception\\TransportException' => $vendorDir . '/symfony/mailer/Exception/TransportException.php', 'Symfony\\Component\\Mailer\\Exception\\TransportExceptionInterface' => $vendorDir . '/symfony/mailer/Exception/TransportExceptionInterface.php', + 'Symfony\\Component\\Mailer\\Exception\\UnexpectedResponseException' => $vendorDir . '/symfony/mailer/Exception/UnexpectedResponseException.php', 'Symfony\\Component\\Mailer\\Exception\\UnsupportedSchemeException' => $vendorDir . '/symfony/mailer/Exception/UnsupportedSchemeException.php', 'Symfony\\Component\\Mailer\\Header\\MetadataHeader' => $vendorDir . '/symfony/mailer/Header/MetadataHeader.php', 'Symfony\\Component\\Mailer\\Header\\TagHeader' => $vendorDir . '/symfony/mailer/Header/TagHeader.php', @@ -3254,6 +3305,9 @@ 'Symfony\\Component\\Mailer\\Messenger\\MessageHandler' => $vendorDir . '/symfony/mailer/Messenger/MessageHandler.php', 'Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage' => $vendorDir . '/symfony/mailer/Messenger/SendEmailMessage.php', 'Symfony\\Component\\Mailer\\SentMessage' => $vendorDir . '/symfony/mailer/SentMessage.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailCount' => $vendorDir . '/symfony/mailer/Test/Constraint/EmailCount.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailIsQueued' => $vendorDir . '/symfony/mailer/Test/Constraint/EmailIsQueued.php', + 'Symfony\\Component\\Mailer\\Test\\TransportFactoryTestCase' => $vendorDir . '/symfony/mailer/Test/TransportFactoryTestCase.php', 'Symfony\\Component\\Mailer\\Transport' => $vendorDir . '/symfony/mailer/Transport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractApiTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractApiTransport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractHttpTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractHttpTransport.php', @@ -3290,6 +3344,7 @@ 'Symfony\\Component\\Mime\\Crypto\\SMimeEncrypter' => $vendorDir . '/symfony/mime/Crypto/SMimeEncrypter.php', 'Symfony\\Component\\Mime\\Crypto\\SMimeSigner' => $vendorDir . '/symfony/mime/Crypto/SMimeSigner.php', 'Symfony\\Component\\Mime\\DependencyInjection\\AddMimeTypeGuesserPass' => $vendorDir . '/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php', + 'Symfony\\Component\\Mime\\DraftEmail' => $vendorDir . '/symfony/mime/DraftEmail.php', 'Symfony\\Component\\Mime\\Email' => $vendorDir . '/symfony/mime/Email.php', 'Symfony\\Component\\Mime\\Encoder\\AddressEncoderInterface' => $vendorDir . '/symfony/mime/Encoder/AddressEncoderInterface.php', 'Symfony\\Component\\Mime\\Encoder\\Base64ContentEncoder' => $vendorDir . '/symfony/mime/Encoder/Base64ContentEncoder.php', @@ -3322,6 +3377,9 @@ 'Symfony\\Component\\Mime\\Header\\ParameterizedHeader' => $vendorDir . '/symfony/mime/Header/ParameterizedHeader.php', 'Symfony\\Component\\Mime\\Header\\PathHeader' => $vendorDir . '/symfony/mime/Header/PathHeader.php', 'Symfony\\Component\\Mime\\Header\\UnstructuredHeader' => $vendorDir . '/symfony/mime/Header/UnstructuredHeader.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\DefaultHtmlToTextConverter' => $vendorDir . '/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface' => $vendorDir . '/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\LeagueHtmlToMarkdownConverter' => $vendorDir . '/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php', 'Symfony\\Component\\Mime\\Message' => $vendorDir . '/symfony/mime/Message.php', 'Symfony\\Component\\Mime\\MessageConverter' => $vendorDir . '/symfony/mime/MessageConverter.php', 'Symfony\\Component\\Mime\\MimeTypeGuesserInterface' => $vendorDir . '/symfony/mime/MimeTypeGuesserInterface.php', @@ -3330,6 +3388,7 @@ 'Symfony\\Component\\Mime\\Part\\AbstractMultipartPart' => $vendorDir . '/symfony/mime/Part/AbstractMultipartPart.php', 'Symfony\\Component\\Mime\\Part\\AbstractPart' => $vendorDir . '/symfony/mime/Part/AbstractPart.php', 'Symfony\\Component\\Mime\\Part\\DataPart' => $vendorDir . '/symfony/mime/Part/DataPart.php', + 'Symfony\\Component\\Mime\\Part\\File' => $vendorDir . '/symfony/mime/Part/File.php', 'Symfony\\Component\\Mime\\Part\\MessagePart' => $vendorDir . '/symfony/mime/Part/MessagePart.php', 'Symfony\\Component\\Mime\\Part\\Multipart\\AlternativePart' => $vendorDir . '/symfony/mime/Part/Multipart/AlternativePart.php', 'Symfony\\Component\\Mime\\Part\\Multipart\\DigestPart' => $vendorDir . '/symfony/mime/Part/Multipart/DigestPart.php', @@ -3339,17 +3398,29 @@ 'Symfony\\Component\\Mime\\Part\\SMimePart' => $vendorDir . '/symfony/mime/Part/SMimePart.php', 'Symfony\\Component\\Mime\\Part\\TextPart' => $vendorDir . '/symfony/mime/Part/TextPart.php', 'Symfony\\Component\\Mime\\RawMessage' => $vendorDir . '/symfony/mime/RawMessage.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailAddressContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailAddressContains.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailAttachmentCount' => $vendorDir . '/symfony/mime/Test/Constraint/EmailAttachmentCount.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHasHeader' => $vendorDir . '/symfony/mime/Test/Constraint/EmailHasHeader.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHeaderSame' => $vendorDir . '/symfony/mime/Test/Constraint/EmailHeaderSame.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHtmlBodyContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailSubjectContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailSubjectContains.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailTextBodyContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailTextBodyContains.php', 'Symfony\\Component\\Process\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/process/Exception/ExceptionInterface.php', 'Symfony\\Component\\Process\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/process/Exception/InvalidArgumentException.php', 'Symfony\\Component\\Process\\Exception\\LogicException' => $vendorDir . '/symfony/process/Exception/LogicException.php', 'Symfony\\Component\\Process\\Exception\\ProcessFailedException' => $vendorDir . '/symfony/process/Exception/ProcessFailedException.php', 'Symfony\\Component\\Process\\Exception\\ProcessSignaledException' => $vendorDir . '/symfony/process/Exception/ProcessSignaledException.php', 'Symfony\\Component\\Process\\Exception\\ProcessTimedOutException' => $vendorDir . '/symfony/process/Exception/ProcessTimedOutException.php', + 'Symfony\\Component\\Process\\Exception\\RunProcessFailedException' => $vendorDir . '/symfony/process/Exception/RunProcessFailedException.php', 'Symfony\\Component\\Process\\Exception\\RuntimeException' => $vendorDir . '/symfony/process/Exception/RuntimeException.php', 'Symfony\\Component\\Process\\ExecutableFinder' => $vendorDir . '/symfony/process/ExecutableFinder.php', 'Symfony\\Component\\Process\\InputStream' => $vendorDir . '/symfony/process/InputStream.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessContext' => $vendorDir . '/symfony/process/Messenger/RunProcessContext.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessage' => $vendorDir . '/symfony/process/Messenger/RunProcessMessage.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessageHandler' => $vendorDir . '/symfony/process/Messenger/RunProcessMessageHandler.php', 'Symfony\\Component\\Process\\PhpExecutableFinder' => $vendorDir . '/symfony/process/PhpExecutableFinder.php', 'Symfony\\Component\\Process\\PhpProcess' => $vendorDir . '/symfony/process/PhpProcess.php', + 'Symfony\\Component\\Process\\PhpSubprocess' => $vendorDir . '/symfony/process/PhpSubprocess.php', 'Symfony\\Component\\Process\\Pipes\\AbstractPipes' => $vendorDir . '/symfony/process/Pipes/AbstractPipes.php', 'Symfony\\Component\\Process\\Pipes\\PipesInterface' => $vendorDir . '/symfony/process/Pipes/PipesInterface.php', 'Symfony\\Component\\Process\\Pipes\\UnixPipes' => $vendorDir . '/symfony/process/Pipes/UnixPipes.php', @@ -3358,7 +3429,9 @@ 'Symfony\\Component\\Process\\ProcessUtils' => $vendorDir . '/symfony/process/ProcessUtils.php', 'Symfony\\Component\\Routing\\Alias' => $vendorDir . '/symfony/routing/Alias.php', 'Symfony\\Component\\Routing\\Annotation\\Route' => $vendorDir . '/symfony/routing/Annotation/Route.php', + 'Symfony\\Component\\Routing\\Attribute\\Route' => $vendorDir . '/symfony/routing/Attribute/Route.php', 'Symfony\\Component\\Routing\\CompiledRoute' => $vendorDir . '/symfony/routing/CompiledRoute.php', + 'Symfony\\Component\\Routing\\DependencyInjection\\AddExpressionLanguageProvidersPass' => $vendorDir . '/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php', 'Symfony\\Component\\Routing\\DependencyInjection\\RoutingResolverPass' => $vendorDir . '/symfony/routing/DependencyInjection/RoutingResolverPass.php', 'Symfony\\Component\\Routing\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/routing/Exception/ExceptionInterface.php', 'Symfony\\Component\\Routing\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/routing/Exception/InvalidArgumentException.php', @@ -3380,6 +3453,9 @@ 'Symfony\\Component\\Routing\\Loader\\AnnotationClassLoader' => $vendorDir . '/symfony/routing/Loader/AnnotationClassLoader.php', 'Symfony\\Component\\Routing\\Loader\\AnnotationDirectoryLoader' => $vendorDir . '/symfony/routing/Loader/AnnotationDirectoryLoader.php', 'Symfony\\Component\\Routing\\Loader\\AnnotationFileLoader' => $vendorDir . '/symfony/routing/Loader/AnnotationFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeClassLoader' => $vendorDir . '/symfony/routing/Loader/AttributeClassLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeDirectoryLoader' => $vendorDir . '/symfony/routing/Loader/AttributeDirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeFileLoader' => $vendorDir . '/symfony/routing/Loader/AttributeFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\ClosureLoader' => $vendorDir . '/symfony/routing/Loader/ClosureLoader.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\AliasConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/AliasConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\CollectionConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/CollectionConfigurator.php', @@ -3396,6 +3472,7 @@ 'Symfony\\Component\\Routing\\Loader\\GlobFileLoader' => $vendorDir . '/symfony/routing/Loader/GlobFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\ObjectLoader' => $vendorDir . '/symfony/routing/Loader/ObjectLoader.php', 'Symfony\\Component\\Routing\\Loader\\PhpFileLoader' => $vendorDir . '/symfony/routing/Loader/PhpFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\Psr4DirectoryLoader' => $vendorDir . '/symfony/routing/Loader/Psr4DirectoryLoader.php', 'Symfony\\Component\\Routing\\Loader\\XmlFileLoader' => $vendorDir . '/symfony/routing/Loader/XmlFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\YamlFileLoader' => $vendorDir . '/symfony/routing/Loader/YamlFileLoader.php', 'Symfony\\Component\\Routing\\Matcher\\CompiledUrlMatcher' => $vendorDir . '/symfony/routing/Matcher/CompiledUrlMatcher.php', @@ -3413,9 +3490,10 @@ 'Symfony\\Component\\Routing\\Matcher\\UrlMatcherInterface' => $vendorDir . '/symfony/routing/Matcher/UrlMatcherInterface.php', 'Symfony\\Component\\Routing\\RequestContext' => $vendorDir . '/symfony/routing/RequestContext.php', 'Symfony\\Component\\Routing\\RequestContextAwareInterface' => $vendorDir . '/symfony/routing/RequestContextAwareInterface.php', + 'Symfony\\Component\\Routing\\Requirement\\EnumRequirement' => $vendorDir . '/symfony/routing/Requirement/EnumRequirement.php', + 'Symfony\\Component\\Routing\\Requirement\\Requirement' => $vendorDir . '/symfony/routing/Requirement/Requirement.php', 'Symfony\\Component\\Routing\\Route' => $vendorDir . '/symfony/routing/Route.php', 'Symfony\\Component\\Routing\\RouteCollection' => $vendorDir . '/symfony/routing/RouteCollection.php', - 'Symfony\\Component\\Routing\\RouteCollectionBuilder' => $vendorDir . '/symfony/routing/RouteCollectionBuilder.php', 'Symfony\\Component\\Routing\\RouteCompiler' => $vendorDir . '/symfony/routing/RouteCompiler.php', 'Symfony\\Component\\Routing\\RouteCompilerInterface' => $vendorDir . '/symfony/routing/RouteCompilerInterface.php', 'Symfony\\Component\\Routing\\Router' => $vendorDir . '/symfony/routing/Router.php', @@ -3558,7 +3636,9 @@ 'Symfony\\Contracts\\Service\\Attribute\\Required' => $vendorDir . '/symfony/service-contracts/Attribute/Required.php', 'Symfony\\Contracts\\Service\\Attribute\\SubscribedService' => $vendorDir . '/symfony/service-contracts/Attribute/SubscribedService.php', 'Symfony\\Contracts\\Service\\ResetInterface' => $vendorDir . '/symfony/service-contracts/ResetInterface.php', + 'Symfony\\Contracts\\Service\\ServiceCollectionInterface' => $vendorDir . '/symfony/service-contracts/ServiceCollectionInterface.php', 'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => $vendorDir . '/symfony/service-contracts/ServiceLocatorTrait.php', + 'Symfony\\Contracts\\Service\\ServiceMethodsSubscriberTrait' => $vendorDir . '/symfony/service-contracts/ServiceMethodsSubscriberTrait.php', 'Symfony\\Contracts\\Service\\ServiceProviderInterface' => $vendorDir . '/symfony/service-contracts/ServiceProviderInterface.php', 'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberInterface.php', 'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberTrait.php', @@ -3574,10 +3654,9 @@ 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\Regex' => $vendorDir . '/symfony/polyfill-intl-idn/Resources/unidata/Regex.php', 'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Normalizer.php', 'Symfony\\Polyfill\\Mbstring\\Mbstring' => $vendorDir . '/symfony/polyfill-mbstring/Mbstring.php', - 'Symfony\\Polyfill\\Php72\\Php72' => $vendorDir . '/symfony/polyfill-php72/Php72.php', - 'Symfony\\Polyfill\\Php73\\Php73' => $vendorDir . '/symfony/polyfill-php73/Php73.php', 'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php', 'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php', + 'Symfony\\Polyfill\\Php83\\Php83' => $vendorDir . '/symfony/polyfill-php83/Php83.php', 'Symfony\\Polyfill\\Uuid\\Uuid' => $vendorDir . '/symfony/polyfill-uuid/Uuid.php', 'System' => $vendorDir . '/pear/pear-core-minimal/src/System.php', 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', diff --git a/composer/autoload_files.php b/composer/autoload_files.php index 7f6361d4c..73f4268cb 100644 --- a/composer/autoload_files.php +++ b/composer/autoload_files.php @@ -8,7 +8,6 @@ return array( '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', - 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', @@ -19,14 +18,14 @@ '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php', '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', - '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', 'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php', - '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', 'b067bc7112e384b61c701452d53a14a8' => $vendorDir . '/mtdowling/jmespath.php/src/JmesPath.php', + '662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php', '8a9dc1de0ca7e01f3e08231539562f61' => $vendorDir . '/aws/aws-sdk-php/src/functions.php', 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php', 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php index bd1edfe07..fd12ad2aa 100644 --- a/composer/autoload_psr4.php +++ b/composer/autoload_psr4.php @@ -16,9 +16,8 @@ 'ZipStreamer\\' => array($vendorDir . '/deepdiver/zipstreamer/src'), 'Webauthn\\' => array($vendorDir . '/web-auth/webauthn-lib/src'), 'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'), + 'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), - 'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'), - 'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 93f632b61..17312fc3c 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -9,7 +9,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 public static $files = array ( '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', - 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', @@ -20,14 +19,14 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php', '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', - '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', 'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php', - '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '09f6b20656683369174dd6fa83b7e5fb' => __DIR__ . '/..' . '/symfony/polyfill-uuid/bootstrap.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', 'b067bc7112e384b61c701452d53a14a8' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/JmesPath.php', + '662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php', '8a9dc1de0ca7e01f3e08231539562f61' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/functions.php', 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php', 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', @@ -73,9 +72,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'S' => array ( 'Symfony\\Polyfill\\Uuid\\' => 22, + 'Symfony\\Polyfill\\Php83\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23, - 'Symfony\\Polyfill\\Php73\\' => 23, - 'Symfony\\Polyfill\\Php72\\' => 23, 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, 'Symfony\\Polyfill\\Intl\\Idn\\' => 26, @@ -233,17 +231,13 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-uuid', ), - 'Symfony\\Polyfill\\Php80\\' => - array ( - 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', - ), - 'Symfony\\Polyfill\\Php73\\' => + 'Symfony\\Polyfill\\Php83\\' => array ( - 0 => __DIR__ . '/..' . '/symfony/polyfill-php73', + 0 => __DIR__ . '/..' . '/symfony/polyfill-php83', ), - 'Symfony\\Polyfill\\Php72\\' => + 'Symfony\\Polyfill\\Php80\\' => array ( - 0 => __DIR__ . '/..' . '/symfony/polyfill-php72', + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', ), 'Symfony\\Polyfill\\Mbstring\\' => array ( @@ -1750,6 +1744,15 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Cose\\Key\\OkpKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/OkpKey.php', 'Cose\\Key\\RsaKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/RsaKey.php', 'Cose\\Key\\SymmetricKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/SymmetricKey.php', + 'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php', + 'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php', + 'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php', + 'DateInvalidTimeZoneException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php', + 'DateMalformedIntervalStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php', + 'DateMalformedPeriodStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php', + 'DateMalformedStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php', + 'DateObjectError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php', + 'DateRangeError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php', 'Doctrine\\Common\\Cache\\Cache' => __DIR__ . '/..' . '/doctrine/cache/lib/Doctrine/Common/Cache/Cache.php', 'Doctrine\\Common\\Cache\\CacheProvider' => __DIR__ . '/..' . '/doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php', 'Doctrine\\Common\\Cache\\ClearableCache' => __DIR__ . '/..' . '/doctrine/cache/lib/Doctrine/Common/Cache/ClearableCache.php', @@ -2352,7 +2355,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'JmesPath\\TreeCompiler' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/TreeCompiler.php', 'JmesPath\\TreeInterpreter' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/TreeInterpreter.php', 'JmesPath\\Utils' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/Utils.php', - 'JsonException' => __DIR__ . '/..' . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', 'JsonSchema\\Constraints\\BaseConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php', 'JsonSchema\\Constraints\\CollectionConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php', 'JsonSchema\\Constraints\\Constraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php', @@ -2700,6 +2702,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenStack\\ObjectStore\\v1\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Params.php', 'OpenStack\\ObjectStore\\v1\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Service.php', 'OpenStack\\OpenStack' => __DIR__ . '/..' . '/php-opencloud/openstack/src/OpenStack.php', + 'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php', 'PEAR' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/PEAR.php', 'PEAR_Error' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/PEAR.php', 'PEAR_ErrorStack' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/PEAR/ErrorStack.php', @@ -2784,6 +2787,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Punic\\Script' => __DIR__ . '/..' . '/punic/punic/src/Script.php', 'Punic\\Territory' => __DIR__ . '/..' . '/punic/punic/src/Territory.php', 'Punic\\Unit' => __DIR__ . '/..' . '/punic/punic/src/Unit.php', + 'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php', 'Sabre\\CalDAV\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php', 'Sabre\\CalDAV\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php', 'Sabre\\CalDAV\\Backend\\NotificationSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php', @@ -3525,19 +3529,25 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Console\\Command\\ListCommand' => __DIR__ . '/..' . '/symfony/console/Command/ListCommand.php', 'Symfony\\Component\\Console\\Command\\LockableTrait' => __DIR__ . '/..' . '/symfony/console/Command/LockableTrait.php', 'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => __DIR__ . '/..' . '/symfony/console/Command/SignalableCommandInterface.php', + 'Symfony\\Component\\Console\\Command\\TraceableCommand' => __DIR__ . '/..' . '/symfony/console/Command/TraceableCommand.php', 'Symfony\\Component\\Console\\Completion\\CompletionInput' => __DIR__ . '/..' . '/symfony/console/Completion/CompletionInput.php', 'Symfony\\Component\\Console\\Completion\\CompletionSuggestions' => __DIR__ . '/..' . '/symfony/console/Completion/CompletionSuggestions.php', 'Symfony\\Component\\Console\\Completion\\Output\\BashCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/BashCompletionOutput.php', 'Symfony\\Component\\Console\\Completion\\Output\\CompletionOutputInterface' => __DIR__ . '/..' . '/symfony/console/Completion/Output/CompletionOutputInterface.php', + 'Symfony\\Component\\Console\\Completion\\Output\\FishCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/FishCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Output\\ZshCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/ZshCompletionOutput.php', 'Symfony\\Component\\Console\\Completion\\Suggestion' => __DIR__ . '/..' . '/symfony/console/Completion/Suggestion.php', 'Symfony\\Component\\Console\\ConsoleEvents' => __DIR__ . '/..' . '/symfony/console/ConsoleEvents.php', 'Symfony\\Component\\Console\\Cursor' => __DIR__ . '/..' . '/symfony/console/Cursor.php', + 'Symfony\\Component\\Console\\DataCollector\\CommandDataCollector' => __DIR__ . '/..' . '/symfony/console/DataCollector/CommandDataCollector.php', + 'Symfony\\Component\\Console\\Debug\\CliRequest' => __DIR__ . '/..' . '/symfony/console/Debug/CliRequest.php', 'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => __DIR__ . '/..' . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php', 'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => __DIR__ . '/..' . '/symfony/console/Descriptor/ApplicationDescription.php', 'Symfony\\Component\\Console\\Descriptor\\Descriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/Descriptor.php', 'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => __DIR__ . '/..' . '/symfony/console/Descriptor/DescriptorInterface.php', 'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/JsonDescriptor.php', 'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/MarkdownDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\ReStructuredTextDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/ReStructuredTextDescriptor.php', 'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/TextDescriptor.php', 'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/XmlDescriptor.php', 'Symfony\\Component\\Console\\EventListener\\ErrorListener' => __DIR__ . '/..' . '/symfony/console/EventListener/ErrorListener.php', @@ -3553,6 +3563,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Console\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/console/Exception/LogicException.php', 'Symfony\\Component\\Console\\Exception\\MissingInputException' => __DIR__ . '/..' . '/symfony/console/Exception/MissingInputException.php', 'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/NamespaceNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\RunCommandFailedException' => __DIR__ . '/..' . '/symfony/console/Exception/RunCommandFailedException.php', 'Symfony\\Component\\Console\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/console/Exception/RuntimeException.php', 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatter.php', 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatterStyle.php', @@ -3570,6 +3581,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Console\\Helper\\HelperInterface' => __DIR__ . '/..' . '/symfony/console/Helper/HelperInterface.php', 'Symfony\\Component\\Console\\Helper\\HelperSet' => __DIR__ . '/..' . '/symfony/console/Helper/HelperSet.php', 'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => __DIR__ . '/..' . '/symfony/console/Helper/InputAwareHelper.php', + 'Symfony\\Component\\Console\\Helper\\OutputWrapper' => __DIR__ . '/..' . '/symfony/console/Helper/OutputWrapper.php', 'Symfony\\Component\\Console\\Helper\\ProcessHelper' => __DIR__ . '/..' . '/symfony/console/Helper/ProcessHelper.php', 'Symfony\\Component\\Console\\Helper\\ProgressBar' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressBar.php', 'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressIndicator.php', @@ -3592,6 +3604,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => __DIR__ . '/..' . '/symfony/console/Input/StreamableInputInterface.php', 'Symfony\\Component\\Console\\Input\\StringInput' => __DIR__ . '/..' . '/symfony/console/Input/StringInput.php', 'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => __DIR__ . '/..' . '/symfony/console/Logger/ConsoleLogger.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandContext' => __DIR__ . '/..' . '/symfony/console/Messenger/RunCommandContext.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessage' => __DIR__ . '/..' . '/symfony/console/Messenger/RunCommandMessage.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessageHandler' => __DIR__ . '/..' . '/symfony/console/Messenger/RunCommandMessageHandler.php', + 'Symfony\\Component\\Console\\Output\\AnsiColorMode' => __DIR__ . '/..' . '/symfony/console/Output/AnsiColorMode.php', 'Symfony\\Component\\Console\\Output\\BufferedOutput' => __DIR__ . '/..' . '/symfony/console/Output/BufferedOutput.php', 'Symfony\\Component\\Console\\Output\\ConsoleOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutput.php', 'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutputInterface.php', @@ -3604,6 +3620,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ChoiceQuestion.php', 'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ConfirmationQuestion.php', 'Symfony\\Component\\Console\\Question\\Question' => __DIR__ . '/..' . '/symfony/console/Question/Question.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalMap' => __DIR__ . '/..' . '/symfony/console/SignalRegistry/SignalMap.php', 'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => __DIR__ . '/..' . '/symfony/console/SignalRegistry/SignalRegistry.php', 'Symfony\\Component\\Console\\SingleCommandApplication' => __DIR__ . '/..' . '/symfony/console/SingleCommandApplication.php', 'Symfony\\Component\\Console\\Style\\OutputStyle' => __DIR__ . '/..' . '/symfony/console/Style/OutputStyle.php', @@ -3685,10 +3702,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\EventDispatcher\\EventSubscriberInterface' => __DIR__ . '/..' . '/symfony/event-dispatcher/EventSubscriberInterface.php', 'Symfony\\Component\\EventDispatcher\\GenericEvent' => __DIR__ . '/..' . '/symfony/event-dispatcher/GenericEvent.php', 'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/ImmutableEventDispatcher.php', - 'Symfony\\Component\\EventDispatcher\\LegacyEventDispatcherProxy' => __DIR__ . '/..' . '/symfony/event-dispatcher/LegacyEventDispatcherProxy.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeader.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeaderItem.php', 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => __DIR__ . '/..' . '/symfony/http-foundation/BinaryFileResponse.php', + 'Symfony\\Component\\HttpFoundation\\ChainRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/ChainRequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\Cookie' => __DIR__ . '/..' . '/symfony/http-foundation/Cookie.php', 'Symfony\\Component\\HttpFoundation\\Exception\\BadRequestException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/BadRequestException.php', 'Symfony\\Component\\HttpFoundation\\Exception\\ConflictingHeadersException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/ConflictingHeadersException.php', @@ -3696,6 +3713,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/RequestExceptionInterface.php', 'Symfony\\Component\\HttpFoundation\\Exception\\SessionNotFoundException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/SessionNotFoundException.php', 'Symfony\\Component\\HttpFoundation\\Exception\\SuspiciousOperationException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/SuspiciousOperationException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/UnexpectedValueException.php', 'Symfony\\Component\\HttpFoundation\\ExpressionRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/ExpressionRequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\FileBag' => __DIR__ . '/..' . '/symfony/http-foundation/FileBag.php', 'Symfony\\Component\\HttpFoundation\\File\\Exception\\AccessDeniedException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/AccessDeniedException.php', @@ -3720,18 +3738,28 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\HttpFoundation\\JsonResponse' => __DIR__ . '/..' . '/symfony/http-foundation/JsonResponse.php', 'Symfony\\Component\\HttpFoundation\\ParameterBag' => __DIR__ . '/..' . '/symfony/http-foundation/ParameterBag.php', 'Symfony\\Component\\HttpFoundation\\RateLimiter\\AbstractRequestRateLimiter' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\PeekableRequestRateLimiterInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php', 'Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php', 'Symfony\\Component\\HttpFoundation\\RedirectResponse' => __DIR__ . '/..' . '/symfony/http-foundation/RedirectResponse.php', 'Symfony\\Component\\HttpFoundation\\Request' => __DIR__ . '/..' . '/symfony/http-foundation/Request.php', 'Symfony\\Component\\HttpFoundation\\RequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\RequestMatcherInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcherInterface.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php', 'Symfony\\Component\\HttpFoundation\\RequestStack' => __DIR__ . '/..' . '/symfony/http-foundation/RequestStack.php', 'Symfony\\Component\\HttpFoundation\\Response' => __DIR__ . '/..' . '/symfony/http-foundation/Response.php', 'Symfony\\Component\\HttpFoundation\\ResponseHeaderBag' => __DIR__ . '/..' . '/symfony/http-foundation/ResponseHeaderBag.php', 'Symfony\\Component\\HttpFoundation\\ServerBag' => __DIR__ . '/..' . '/symfony/http-foundation/ServerBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/AttributeBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php', - 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\FlashBagAwareSessionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php', 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/FlashBag.php', 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/FlashBagInterface.php', @@ -3764,19 +3792,35 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php', - 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\ServiceSessionFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php', 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php', + 'Symfony\\Component\\HttpFoundation\\StreamedJsonResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedJsonResponse.php', 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedResponse.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\RequestAttributeValueSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseCookieValueSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseFormatSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasCookie' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasHeader' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderLocationSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsRedirected' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsSuccessful' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsUnprocessable' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseStatusCodeSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php', + 'Symfony\\Component\\HttpFoundation\\UriSigner' => __DIR__ . '/..' . '/symfony/http-foundation/UriSigner.php', 'Symfony\\Component\\HttpFoundation\\UrlHelper' => __DIR__ . '/..' . '/symfony/http-foundation/UrlHelper.php', + 'Symfony\\Component\\Mailer\\Command\\MailerTestCommand' => __DIR__ . '/..' . '/symfony/mailer/Command/MailerTestCommand.php', 'Symfony\\Component\\Mailer\\DataCollector\\MessageDataCollector' => __DIR__ . '/..' . '/symfony/mailer/DataCollector/MessageDataCollector.php', 'Symfony\\Component\\Mailer\\DelayedEnvelope' => __DIR__ . '/..' . '/symfony/mailer/DelayedEnvelope.php', 'Symfony\\Component\\Mailer\\Envelope' => __DIR__ . '/..' . '/symfony/mailer/Envelope.php', 'Symfony\\Component\\Mailer\\EventListener\\EnvelopeListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/EnvelopeListener.php', 'Symfony\\Component\\Mailer\\EventListener\\MessageListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/MessageListener.php', 'Symfony\\Component\\Mailer\\EventListener\\MessageLoggerListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/MessageLoggerListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessengerTransportListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/MessengerTransportListener.php', + 'Symfony\\Component\\Mailer\\Event\\FailedMessageEvent' => __DIR__ . '/..' . '/symfony/mailer/Event/FailedMessageEvent.php', 'Symfony\\Component\\Mailer\\Event\\MessageEvent' => __DIR__ . '/..' . '/symfony/mailer/Event/MessageEvent.php', 'Symfony\\Component\\Mailer\\Event\\MessageEvents' => __DIR__ . '/..' . '/symfony/mailer/Event/MessageEvents.php', + 'Symfony\\Component\\Mailer\\Event\\SentMessageEvent' => __DIR__ . '/..' . '/symfony/mailer/Event/SentMessageEvent.php', 'Symfony\\Component\\Mailer\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/mailer/Exception/ExceptionInterface.php', 'Symfony\\Component\\Mailer\\Exception\\HttpTransportException' => __DIR__ . '/..' . '/symfony/mailer/Exception/HttpTransportException.php', 'Symfony\\Component\\Mailer\\Exception\\IncompleteDsnException' => __DIR__ . '/..' . '/symfony/mailer/Exception/IncompleteDsnException.php', @@ -3785,6 +3829,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mailer\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/mailer/Exception/RuntimeException.php', 'Symfony\\Component\\Mailer\\Exception\\TransportException' => __DIR__ . '/..' . '/symfony/mailer/Exception/TransportException.php', 'Symfony\\Component\\Mailer\\Exception\\TransportExceptionInterface' => __DIR__ . '/..' . '/symfony/mailer/Exception/TransportExceptionInterface.php', + 'Symfony\\Component\\Mailer\\Exception\\UnexpectedResponseException' => __DIR__ . '/..' . '/symfony/mailer/Exception/UnexpectedResponseException.php', 'Symfony\\Component\\Mailer\\Exception\\UnsupportedSchemeException' => __DIR__ . '/..' . '/symfony/mailer/Exception/UnsupportedSchemeException.php', 'Symfony\\Component\\Mailer\\Header\\MetadataHeader' => __DIR__ . '/..' . '/symfony/mailer/Header/MetadataHeader.php', 'Symfony\\Component\\Mailer\\Header\\TagHeader' => __DIR__ . '/..' . '/symfony/mailer/Header/TagHeader.php', @@ -3793,6 +3838,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mailer\\Messenger\\MessageHandler' => __DIR__ . '/..' . '/symfony/mailer/Messenger/MessageHandler.php', 'Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage' => __DIR__ . '/..' . '/symfony/mailer/Messenger/SendEmailMessage.php', 'Symfony\\Component\\Mailer\\SentMessage' => __DIR__ . '/..' . '/symfony/mailer/SentMessage.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailCount' => __DIR__ . '/..' . '/symfony/mailer/Test/Constraint/EmailCount.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailIsQueued' => __DIR__ . '/..' . '/symfony/mailer/Test/Constraint/EmailIsQueued.php', + 'Symfony\\Component\\Mailer\\Test\\TransportFactoryTestCase' => __DIR__ . '/..' . '/symfony/mailer/Test/TransportFactoryTestCase.php', 'Symfony\\Component\\Mailer\\Transport' => __DIR__ . '/..' . '/symfony/mailer/Transport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractApiTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractApiTransport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractHttpTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractHttpTransport.php', @@ -3829,6 +3877,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mime\\Crypto\\SMimeEncrypter' => __DIR__ . '/..' . '/symfony/mime/Crypto/SMimeEncrypter.php', 'Symfony\\Component\\Mime\\Crypto\\SMimeSigner' => __DIR__ . '/..' . '/symfony/mime/Crypto/SMimeSigner.php', 'Symfony\\Component\\Mime\\DependencyInjection\\AddMimeTypeGuesserPass' => __DIR__ . '/..' . '/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php', + 'Symfony\\Component\\Mime\\DraftEmail' => __DIR__ . '/..' . '/symfony/mime/DraftEmail.php', 'Symfony\\Component\\Mime\\Email' => __DIR__ . '/..' . '/symfony/mime/Email.php', 'Symfony\\Component\\Mime\\Encoder\\AddressEncoderInterface' => __DIR__ . '/..' . '/symfony/mime/Encoder/AddressEncoderInterface.php', 'Symfony\\Component\\Mime\\Encoder\\Base64ContentEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/Base64ContentEncoder.php', @@ -3861,6 +3910,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mime\\Header\\ParameterizedHeader' => __DIR__ . '/..' . '/symfony/mime/Header/ParameterizedHeader.php', 'Symfony\\Component\\Mime\\Header\\PathHeader' => __DIR__ . '/..' . '/symfony/mime/Header/PathHeader.php', 'Symfony\\Component\\Mime\\Header\\UnstructuredHeader' => __DIR__ . '/..' . '/symfony/mime/Header/UnstructuredHeader.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\DefaultHtmlToTextConverter' => __DIR__ . '/..' . '/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface' => __DIR__ . '/..' . '/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\LeagueHtmlToMarkdownConverter' => __DIR__ . '/..' . '/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php', 'Symfony\\Component\\Mime\\Message' => __DIR__ . '/..' . '/symfony/mime/Message.php', 'Symfony\\Component\\Mime\\MessageConverter' => __DIR__ . '/..' . '/symfony/mime/MessageConverter.php', 'Symfony\\Component\\Mime\\MimeTypeGuesserInterface' => __DIR__ . '/..' . '/symfony/mime/MimeTypeGuesserInterface.php', @@ -3869,6 +3921,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mime\\Part\\AbstractMultipartPart' => __DIR__ . '/..' . '/symfony/mime/Part/AbstractMultipartPart.php', 'Symfony\\Component\\Mime\\Part\\AbstractPart' => __DIR__ . '/..' . '/symfony/mime/Part/AbstractPart.php', 'Symfony\\Component\\Mime\\Part\\DataPart' => __DIR__ . '/..' . '/symfony/mime/Part/DataPart.php', + 'Symfony\\Component\\Mime\\Part\\File' => __DIR__ . '/..' . '/symfony/mime/Part/File.php', 'Symfony\\Component\\Mime\\Part\\MessagePart' => __DIR__ . '/..' . '/symfony/mime/Part/MessagePart.php', 'Symfony\\Component\\Mime\\Part\\Multipart\\AlternativePart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/AlternativePart.php', 'Symfony\\Component\\Mime\\Part\\Multipart\\DigestPart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/DigestPart.php', @@ -3878,17 +3931,29 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mime\\Part\\SMimePart' => __DIR__ . '/..' . '/symfony/mime/Part/SMimePart.php', 'Symfony\\Component\\Mime\\Part\\TextPart' => __DIR__ . '/..' . '/symfony/mime/Part/TextPart.php', 'Symfony\\Component\\Mime\\RawMessage' => __DIR__ . '/..' . '/symfony/mime/RawMessage.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailAddressContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailAddressContains.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailAttachmentCount' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailAttachmentCount.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHasHeader' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailHasHeader.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHeaderSame' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailHeaderSame.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHtmlBodyContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailSubjectContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailSubjectContains.php', + 'Symfony\\Component\\Mime\\Test\\Constraint\\EmailTextBodyContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailTextBodyContains.php', 'Symfony\\Component\\Process\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/process/Exception/ExceptionInterface.php', 'Symfony\\Component\\Process\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/process/Exception/InvalidArgumentException.php', 'Symfony\\Component\\Process\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/process/Exception/LogicException.php', 'Symfony\\Component\\Process\\Exception\\ProcessFailedException' => __DIR__ . '/..' . '/symfony/process/Exception/ProcessFailedException.php', 'Symfony\\Component\\Process\\Exception\\ProcessSignaledException' => __DIR__ . '/..' . '/symfony/process/Exception/ProcessSignaledException.php', 'Symfony\\Component\\Process\\Exception\\ProcessTimedOutException' => __DIR__ . '/..' . '/symfony/process/Exception/ProcessTimedOutException.php', + 'Symfony\\Component\\Process\\Exception\\RunProcessFailedException' => __DIR__ . '/..' . '/symfony/process/Exception/RunProcessFailedException.php', 'Symfony\\Component\\Process\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/process/Exception/RuntimeException.php', 'Symfony\\Component\\Process\\ExecutableFinder' => __DIR__ . '/..' . '/symfony/process/ExecutableFinder.php', 'Symfony\\Component\\Process\\InputStream' => __DIR__ . '/..' . '/symfony/process/InputStream.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessContext' => __DIR__ . '/..' . '/symfony/process/Messenger/RunProcessContext.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessage' => __DIR__ . '/..' . '/symfony/process/Messenger/RunProcessMessage.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessageHandler' => __DIR__ . '/..' . '/symfony/process/Messenger/RunProcessMessageHandler.php', 'Symfony\\Component\\Process\\PhpExecutableFinder' => __DIR__ . '/..' . '/symfony/process/PhpExecutableFinder.php', 'Symfony\\Component\\Process\\PhpProcess' => __DIR__ . '/..' . '/symfony/process/PhpProcess.php', + 'Symfony\\Component\\Process\\PhpSubprocess' => __DIR__ . '/..' . '/symfony/process/PhpSubprocess.php', 'Symfony\\Component\\Process\\Pipes\\AbstractPipes' => __DIR__ . '/..' . '/symfony/process/Pipes/AbstractPipes.php', 'Symfony\\Component\\Process\\Pipes\\PipesInterface' => __DIR__ . '/..' . '/symfony/process/Pipes/PipesInterface.php', 'Symfony\\Component\\Process\\Pipes\\UnixPipes' => __DIR__ . '/..' . '/symfony/process/Pipes/UnixPipes.php', @@ -3897,7 +3962,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Process\\ProcessUtils' => __DIR__ . '/..' . '/symfony/process/ProcessUtils.php', 'Symfony\\Component\\Routing\\Alias' => __DIR__ . '/..' . '/symfony/routing/Alias.php', 'Symfony\\Component\\Routing\\Annotation\\Route' => __DIR__ . '/..' . '/symfony/routing/Annotation/Route.php', + 'Symfony\\Component\\Routing\\Attribute\\Route' => __DIR__ . '/..' . '/symfony/routing/Attribute/Route.php', 'Symfony\\Component\\Routing\\CompiledRoute' => __DIR__ . '/..' . '/symfony/routing/CompiledRoute.php', + 'Symfony\\Component\\Routing\\DependencyInjection\\AddExpressionLanguageProvidersPass' => __DIR__ . '/..' . '/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php', 'Symfony\\Component\\Routing\\DependencyInjection\\RoutingResolverPass' => __DIR__ . '/..' . '/symfony/routing/DependencyInjection/RoutingResolverPass.php', 'Symfony\\Component\\Routing\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/routing/Exception/ExceptionInterface.php', 'Symfony\\Component\\Routing\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/routing/Exception/InvalidArgumentException.php', @@ -3919,6 +3986,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Routing\\Loader\\AnnotationClassLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AnnotationClassLoader.php', 'Symfony\\Component\\Routing\\Loader\\AnnotationDirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AnnotationDirectoryLoader.php', 'Symfony\\Component\\Routing\\Loader\\AnnotationFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AnnotationFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeClassLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AttributeClassLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeDirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AttributeDirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AttributeFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\ClosureLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ClosureLoader.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\AliasConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/AliasConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\CollectionConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/CollectionConfigurator.php', @@ -3935,6 +4005,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Routing\\Loader\\GlobFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/GlobFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\ObjectLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ObjectLoader.php', 'Symfony\\Component\\Routing\\Loader\\PhpFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/PhpFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\Psr4DirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/Psr4DirectoryLoader.php', 'Symfony\\Component\\Routing\\Loader\\XmlFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/XmlFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\YamlFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/YamlFileLoader.php', 'Symfony\\Component\\Routing\\Matcher\\CompiledUrlMatcher' => __DIR__ . '/..' . '/symfony/routing/Matcher/CompiledUrlMatcher.php', @@ -3952,9 +4023,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Routing\\Matcher\\UrlMatcherInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/UrlMatcherInterface.php', 'Symfony\\Component\\Routing\\RequestContext' => __DIR__ . '/..' . '/symfony/routing/RequestContext.php', 'Symfony\\Component\\Routing\\RequestContextAwareInterface' => __DIR__ . '/..' . '/symfony/routing/RequestContextAwareInterface.php', + 'Symfony\\Component\\Routing\\Requirement\\EnumRequirement' => __DIR__ . '/..' . '/symfony/routing/Requirement/EnumRequirement.php', + 'Symfony\\Component\\Routing\\Requirement\\Requirement' => __DIR__ . '/..' . '/symfony/routing/Requirement/Requirement.php', 'Symfony\\Component\\Routing\\Route' => __DIR__ . '/..' . '/symfony/routing/Route.php', 'Symfony\\Component\\Routing\\RouteCollection' => __DIR__ . '/..' . '/symfony/routing/RouteCollection.php', - 'Symfony\\Component\\Routing\\RouteCollectionBuilder' => __DIR__ . '/..' . '/symfony/routing/RouteCollectionBuilder.php', 'Symfony\\Component\\Routing\\RouteCompiler' => __DIR__ . '/..' . '/symfony/routing/RouteCompiler.php', 'Symfony\\Component\\Routing\\RouteCompilerInterface' => __DIR__ . '/..' . '/symfony/routing/RouteCompilerInterface.php', 'Symfony\\Component\\Routing\\Router' => __DIR__ . '/..' . '/symfony/routing/Router.php', @@ -4097,7 +4169,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Contracts\\Service\\Attribute\\Required' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/Required.php', 'Symfony\\Contracts\\Service\\Attribute\\SubscribedService' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/SubscribedService.php', 'Symfony\\Contracts\\Service\\ResetInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ResetInterface.php', + 'Symfony\\Contracts\\Service\\ServiceCollectionInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceCollectionInterface.php', 'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceLocatorTrait.php', + 'Symfony\\Contracts\\Service\\ServiceMethodsSubscriberTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceMethodsSubscriberTrait.php', 'Symfony\\Contracts\\Service\\ServiceProviderInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceProviderInterface.php', 'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberInterface.php', 'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberTrait.php', @@ -4113,10 +4187,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\Regex' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Resources/unidata/Regex.php', 'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Normalizer.php', 'Symfony\\Polyfill\\Mbstring\\Mbstring' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/Mbstring.php', - 'Symfony\\Polyfill\\Php72\\Php72' => __DIR__ . '/..' . '/symfony/polyfill-php72/Php72.php', - 'Symfony\\Polyfill\\Php73\\Php73' => __DIR__ . '/..' . '/symfony/polyfill-php73/Php73.php', 'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php', 'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php', + 'Symfony\\Polyfill\\Php83\\Php83' => __DIR__ . '/..' . '/symfony/polyfill-php83/Php83.php', 'Symfony\\Polyfill\\Uuid\\Uuid' => __DIR__ . '/..' . '/symfony/polyfill-uuid/Uuid.php', 'System' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/System.php', 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', diff --git a/composer/installed.json b/composer/installed.json index 117de366f..cc01983e0 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -3443,27 +3443,27 @@ }, { "name": "psr/log", - "version": "2.0.0", - "version_normalized": "2.0.0.0", + "version": "3.0.2", + "version_normalized": "3.0.2.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", - "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", + "url": "https://github.com/gitapi/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { "php": ">=8.0.0" }, - "time": "2021-07-14T16:41:46+00:00", + "time": "2024-09-11T13:17:53+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.x-dev" } }, "installation-source": "dist", @@ -3490,7 +3490,7 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/2.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, "install-path": "../psr/log" }, @@ -4385,31 +4385,32 @@ }, { "name": "stecman/symfony-console-completion", - "version": "0.11.0", - "version_normalized": "0.11.0.0", + "version": "v0.13.0", + "version_normalized": "0.13.0.0", "source": { "type": "git", "url": "https://github.com/stecman/symfony-console-completion.git", - "reference": "a9502dab59405e275a9f264536c4e1cb61fc3518" + "reference": "769e6b49123847648ae80f10d3ccb0f128cbb953" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/stecman/symfony-console-completion/zipball/a9502dab59405e275a9f264536c4e1cb61fc3518", - "reference": "a9502dab59405e275a9f264536c4e1cb61fc3518", + "url": "https://github.com/gitapi/repos/stecman/symfony-console-completion/zipball/769e6b49123847648ae80f10d3ccb0f128cbb953", + "reference": "769e6b49123847648ae80f10d3ccb0f128cbb953", "shasum": "" }, "require": { - "php": ">=5.3.2", - "symfony/console": "~2.3 || ~3.0 || ~4.0 || ~5.0" + "php": ">=8.0.2", + "symfony/console": "~6.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8.36 || ~5.7 || ~6.4" + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "phpunit/phpunit": "^9.5" }, - "time": "2019-11-24T17:03:06+00:00", + "time": "2024-07-05T11:49:57+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "0.10.x-dev" + "dev-master": "0.14.x-dev" } }, "installation-source": "dist", @@ -4431,61 +4432,56 @@ "description": "Automatic BASH completion for Symfony Console Component based applications.", "support": { "issues": "https://github.com/stecman/symfony-console-completion/issues", - "source": "https://github.com/stecman/symfony-console-completion/tree/0.11.0" + "source": "https://github.com/stecman/symfony-console-completion/tree/v0.13.0" }, "install-path": "../stecman/symfony-console-completion" }, { "name": "symfony/console", - "version": "v5.4.35", - "version_normalized": "5.4.35.0", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931" + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931", + "url": "https://github.com/gitapi/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" }, "provide": { - "psr/log-implementation": "1.0|2.0" + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "time": "2024-01-23T14:28:09+00:00", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "time": "2024-09-20T08:15:52+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -4519,7 +4515,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.35" + "source": "https://github.com/symfony/console/tree/v6.4.12" }, "funding": [ { @@ -4747,47 +4743,42 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.4.26", - "version_normalized": "5.4.26.0", + "version": "v6.4.8", + "version_normalized": "6.4.8.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac" + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/event-dispatcher/zipball/5dcc00e03413f05c1e7900090927bb7247cb0aac", - "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac", + "url": "https://github.com/gitapi/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, - "time": "2023-07-06T06:34:20+00:00", + "time": "2024-05-31T14:49:08+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -4815,7 +4806,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.26" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" }, "funding": [ { @@ -4835,31 +4826,28 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.0.2", - "version_normalized": "3.0.2.0", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/event-dispatcher-contracts/zipball/7bc61cc2db649b4637d331240c5346dcc7708051", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051", + "url": "https://github.com/gitapi/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, - "time": "2022-01-02T09:55:41+00:00", + "time": "2024-04-18T09:32:20+00:00", "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -4897,7 +4885,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -4917,38 +4905,39 @@ }, { "name": "symfony/http-foundation", - "version": "v5.4.25", - "version_normalized": "5.4.25.0", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b" + "reference": "133ac043875f59c26c55e79cf074562127cce4d2" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/http-foundation/zipball/f66be2706075c5f6325d2fe2b743a57fb5d23f6b", - "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b", + "url": "https://github.com/gitapi/repos/symfony/http-foundation/zipball/133ac043875f59c26c55e79cf074562127cce4d2", + "reference": "133ac043875f59c26c55e79cf074562127cce4d2", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-php83": "^1.27" }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" + "conflict": { + "symfony/cache": "<6.3" }, - "suggest": { - "symfony/mime": "To use the file extension guesser" + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, - "time": "2023-06-22T08:06:06+00:00", + "time": "2024-09-20T08:18:25+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -4976,7 +4965,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.25" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.12" }, "funding": [ { @@ -4996,38 +4985,42 @@ }, { "name": "symfony/mailer", - "version": "v5.4.22", - "version_normalized": "5.4.22.0", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b" + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/mailer/zipball/6330cd465dfd8b7a07515757a1c37069075f7b0b", - "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b", + "url": "https://github.com/gitapi/repos/symfony/mailer/zipball/b6a25408c569ae2366b3f663a4edad19420a9c26", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/mime": "^5.2.6|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/http-kernel": "<4.4" + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" }, "require-dev": { - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" }, - "time": "2023-03-10T10:15:32+00:00", + "time": "2024-09-08T12:30:05+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5055,7 +5048,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v5.4.22" + "source": "https://github.com/symfony/mailer/tree/v6.4.12" }, "funding": [ { @@ -5075,42 +5068,43 @@ }, { "name": "symfony/mime", - "version": "v5.4.19", - "version_normalized": "5.4.19.0", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a858429a9c704edc53fe057228cf9ca282ba48eb" + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/mime/zipball/a858429a9c704edc53fe057228cf9ca282ba48eb", - "reference": "a858429a9c704edc53fe057228cf9ca282ba48eb", + "url": "https://github.com/gitapi/repos/symfony/mime/zipball/abe16ee7790b16aa525877419deb0f113953f0e1", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<4.4", - "symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6" + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/property-access": "^4.4|^5.1|^6.0", - "symfony/property-info": "^4.4|^5.1|^6.0", - "symfony/serializer": "^5.4.14|~6.0.14|^6.1.6" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, - "time": "2023-01-09T05:43:46+00:00", + "time": "2024-09-20T08:18:25+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5142,7 +5136,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.19" + "source": "https://github.com/symfony/mime/tree/v6.4.12" }, "funding": [ { @@ -5244,26 +5238,26 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", - "version_normalized": "1.29.0.0", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, - "time": "2024-01-29T20:11:03+00:00", + "time": "2024-09-09T11:45:10+00:00", "type": "library", "extra": { "thanks": { @@ -5305,7 +5299,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -5325,28 +5319,27 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.29.0", - "version_normalized": "1.29.0.0", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" }, - "time": "2024-01-29T20:11:03+00:00", + "time": "2024-09-09T11:45:10+00:00", "type": "library", "extra": { "thanks": { @@ -5392,7 +5385,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -5412,26 +5405,26 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", - "version_normalized": "1.29.0.0", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://github.com/gitapi/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, - "time": "2024-01-29T20:11:03+00:00", + "time": "2024-09-09T11:45:10+00:00", "type": "library", "extra": { "thanks": { @@ -5476,7 +5469,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -5578,18 +5571,18 @@ "install-path": "../symfony/polyfill-mbstring" }, { - "name": "symfony/polyfill-php72", + "name": "symfony/polyfill-php80", "version": "v1.29.0", "version_normalized": "1.29.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", + "url": "https://github.com/gitapi/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -5609,86 +5602,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "install-path": "../symfony/polyfill-php72" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.28.0", - "version_normalized": "1.28.0.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" - }, - "dist": { - "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "time": "2023-01-26T09:26:14+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "installation-source": "dist", - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, "classmap": [ "Resources/stubs" @@ -5699,6 +5613,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -5708,7 +5626,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -5717,7 +5635,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -5733,27 +5651,27 @@ "type": "tidelift" } ], - "install-path": "../symfony/polyfill-php73" + "install-path": "../symfony/polyfill-php80" }, { - "name": "symfony/polyfill-php80", - "version": "v1.29.0", - "version_normalized": "1.29.0.0", + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://github.com/gitapi/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, - "time": "2024-01-29T20:11:03+00:00", + "time": "2024-09-09T11:45:10+00:00", "type": "library", "extra": { "thanks": { @@ -5767,7 +5685,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -5778,10 +5696,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -5791,7 +5705,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -5800,7 +5714,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -5816,7 +5730,7 @@ "type": "tidelift" } ], - "install-path": "../symfony/polyfill-php80" + "install-path": "../symfony/polyfill-php83" }, { "name": "symfony/polyfill-uuid", @@ -5902,24 +5816,23 @@ }, { "name": "symfony/process", - "version": "v5.4.34", - "version_normalized": "5.4.34.0", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a" + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/process/zipball/8fa22178dfc368911dbd513b431cd9b06f9afe7a", - "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a", + "url": "https://github.com/gitapi/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3", + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, - "time": "2023-12-02T08:41:43+00:00", + "time": "2024-09-17T12:47:12+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5947,7 +5860,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.34" + "source": "https://github.com/symfony/process/tree/v6.4.12" }, "funding": [ { @@ -5967,46 +5880,39 @@ }, { "name": "symfony/routing", - "version": "v5.4.25", - "version_normalized": "5.4.25.0", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649" + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/routing/zipball/56bfc1394f7011303eb2e22724f9b422d3f14649", - "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649", + "url": "https://github.com/gitapi/repos/symfony/routing/zipball/a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.12", - "symfony/config": "<5.3", - "symfony/dependency-injection": "<4.4", - "symfony/yaml": "<4.4" + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^5.3|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "For using the all-in-one router or any loader", - "symfony/expression-language": "For using expression matching", - "symfony/http-foundation": "For using a Symfony Request object", - "symfony/yaml": "For using the YAML loader" + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, - "time": "2023-06-05T14:18:47+00:00", + "time": "2024-09-20T08:32:26+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6040,7 +5946,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v5.4.25" + "source": "https://github.com/symfony/routing/tree/v6.4.12" }, "funding": [ { @@ -6060,34 +5966,32 @@ }, { "name": "symfony/service-contracts", - "version": "v3.0.2", - "version_normalized": "3.0.2.0", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://github.com/gitapi/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "url": "https://github.com/gitapi/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { - "php": ">=8.0.2", - "psr/container": "^2.0" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, - "time": "2022-05-30T19:17:58+00:00", + "time": "2024-04-18T09:32:20+00:00", "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -6098,7 +6002,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6125,7 +6032,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { diff --git a/composer/installed.php b/composer/installed.php index 889b5db05..7c035af61 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -512,9 +512,9 @@ ), ), 'psr/log' => array( - 'pretty_version' => '2.0.0', - 'version' => '2.0.0.0', - 'reference' => 'ef29f6d262798707a9edd554e2b82517ef3a9376', + 'pretty_version' => '3.0.2', + 'version' => '3.0.2.0', + 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), @@ -523,7 +523,7 @@ 'psr/log-implementation' => array( 'dev_requirement' => false, 'provided' => array( - 0 => '1.0|2.0', + 0 => '1.0|2.0|3.0', ), ), 'punic/calendar' => array( @@ -644,18 +644,18 @@ 'dev_requirement' => false, ), 'stecman/symfony-console-completion' => array( - 'pretty_version' => '0.11.0', - 'version' => '0.11.0.0', - 'reference' => 'a9502dab59405e275a9f264536c4e1cb61fc3518', + 'pretty_version' => 'v0.13.0', + 'version' => '0.13.0.0', + 'reference' => '769e6b49123847648ae80f10d3ccb0f128cbb953', 'type' => 'library', 'install_path' => __DIR__ . '/../stecman/symfony-console-completion', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/console' => array( - 'pretty_version' => 'v5.4.35', - 'version' => '5.4.35.0', - 'reference' => 'dbdf6adcb88d5f83790e1efb57ef4074309d3931', + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => '72d080eb9edf80e36c19be61f72c98ed8273b765', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/console', 'aliases' => array(), @@ -689,18 +689,18 @@ 'dev_requirement' => false, ), 'symfony/event-dispatcher' => array( - 'pretty_version' => 'v5.4.26', - 'version' => '5.4.26.0', - 'reference' => '5dcc00e03413f05c1e7900090927bb7247cb0aac', + 'pretty_version' => 'v6.4.8', + 'version' => '6.4.8.0', + 'reference' => '8d7507f02b06e06815e56bb39aa0128e3806208b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/event-dispatcher', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/event-dispatcher-contracts' => array( - 'pretty_version' => 'v3.0.2', - 'version' => '3.0.2.0', - 'reference' => '7bc61cc2db649b4637d331240c5346dcc7708051', + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => '8f93aec25d41b72493c6ddff14e916177c9efc50', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/event-dispatcher-contracts', 'aliases' => array(), @@ -709,31 +709,31 @@ 'symfony/event-dispatcher-implementation' => array( 'dev_requirement' => false, 'provided' => array( - 0 => '2.0', + 0 => '2.0|3.0', ), ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v5.4.25', - 'version' => '5.4.25.0', - 'reference' => 'f66be2706075c5f6325d2fe2b743a57fb5d23f6b', + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => '133ac043875f59c26c55e79cf074562127cce4d2', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/mailer' => array( - 'pretty_version' => 'v5.4.22', - 'version' => '5.4.22.0', - 'reference' => '6330cd465dfd8b7a07515757a1c37069075f7b0b', + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => 'b6a25408c569ae2366b3f663a4edad19420a9c26', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/mailer', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/mime' => array( - 'pretty_version' => 'v5.4.19', - 'version' => '5.4.19.0', - 'reference' => 'a858429a9c704edc53fe057228cf9ca282ba48eb', + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => 'abe16ee7790b16aa525877419deb0f113953f0e1', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/mime', 'aliases' => array(), @@ -749,27 +749,27 @@ 'dev_requirement' => false, ), 'symfony/polyfill-intl-grapheme' => array( - 'pretty_version' => 'v1.29.0', - 'version' => '1.29.0.0', - 'reference' => '32a9da87d7b3245e09ac426c83d334ae9f06f80f', + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => 'b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/polyfill-intl-idn' => array( - 'pretty_version' => 'v1.29.0', - 'version' => '1.29.0.0', - 'reference' => 'a287ed7475f85bf6f61890146edbc932c0fff919', + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => 'c36586dcf89a12315939e00ec9b4474adcb1d773', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/polyfill-intl-normalizer' => array( - 'pretty_version' => 'v1.29.0', - 'version' => '1.29.0.0', - 'reference' => 'bc45c394692b948b4d383a08d7753968bed9a83d', + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => '3833d7255cc303546435cb650316bff708a1c75c', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer', 'aliases' => array(), @@ -784,30 +784,21 @@ 'aliases' => array(), 'dev_requirement' => false, ), - 'symfony/polyfill-php72' => array( + 'symfony/polyfill-php80' => array( 'pretty_version' => 'v1.29.0', 'version' => '1.29.0.0', - 'reference' => '861391a8da9a04cbad2d232ddd9e4893220d6e25', - 'type' => 'library', - 'install_path' => __DIR__ . '/../symfony/polyfill-php72', - 'aliases' => array(), - 'dev_requirement' => false, - ), - 'symfony/polyfill-php73' => array( - 'pretty_version' => 'v1.28.0', - 'version' => '1.28.0.0', - 'reference' => 'fe2f306d1d9d346a7fee353d0d5012e401e984b5', + 'reference' => '87b68208d5c1188808dd7839ee1e6c8ec3b02f1b', 'type' => 'library', - 'install_path' => __DIR__ . '/../symfony/polyfill-php73', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'aliases' => array(), 'dev_requirement' => false, ), - 'symfony/polyfill-php80' => array( - 'pretty_version' => 'v1.29.0', - 'version' => '1.29.0.0', - 'reference' => '87b68208d5c1188808dd7839ee1e6c8ec3b02f1b', + 'symfony/polyfill-php83' => array( + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => '2fb86d65e2d424369ad2905e83b236a8805ba491', 'type' => 'library', - 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'install_path' => __DIR__ . '/../symfony/polyfill-php83', 'aliases' => array(), 'dev_requirement' => false, ), @@ -821,27 +812,27 @@ 'dev_requirement' => false, ), 'symfony/process' => array( - 'pretty_version' => 'v5.4.34', - 'version' => '5.4.34.0', - 'reference' => '8fa22178dfc368911dbd513b431cd9b06f9afe7a', + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => '3f94e5f13ff58df371a7ead461b6e8068900fbb3', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/process', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/routing' => array( - 'pretty_version' => 'v5.4.25', - 'version' => '5.4.25.0', - 'reference' => '56bfc1394f7011303eb2e22724f9b422d3f14649', + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => 'a7c8036bd159486228dc9be3e846a00a0dda9f9f', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/routing', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/service-contracts' => array( - 'pretty_version' => 'v3.0.2', - 'version' => '3.0.2.0', - 'reference' => 'd78d39c1599bd1188b8e26bb341da52c3c6d8a66', + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => 'bd1d9e59a81d8fa4acdcea3f617c581f7475a80f', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/service-contracts', 'aliases' => array(), diff --git a/psr/log/src/LoggerAwareInterface.php b/psr/log/src/LoggerAwareInterface.php index 4d64f4786..062187057 100644 --- a/psr/log/src/LoggerAwareInterface.php +++ b/psr/log/src/LoggerAwareInterface.php @@ -9,10 +9,6 @@ interface LoggerAwareInterface { /** * Sets a logger instance on the object. - * - * @param LoggerInterface $logger - * - * @return void */ - public function setLogger(LoggerInterface $logger); + public function setLogger(LoggerInterface $logger): void; } diff --git a/psr/log/src/LoggerAwareTrait.php b/psr/log/src/LoggerAwareTrait.php index 5f1553a4c..85104dbc1 100644 --- a/psr/log/src/LoggerAwareTrait.php +++ b/psr/log/src/LoggerAwareTrait.php @@ -9,17 +9,13 @@ trait LoggerAwareTrait { /** * The logger instance. - * - * @var LoggerInterface|null */ protected ?LoggerInterface $logger = null; /** * Sets a logger. - * - * @param LoggerInterface $logger */ - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } diff --git a/psr/log/src/LoggerInterface.php b/psr/log/src/LoggerInterface.php index b4d062b9b..cb4cf648b 100644 --- a/psr/log/src/LoggerInterface.php +++ b/psr/log/src/LoggerInterface.php @@ -22,12 +22,9 @@ interface LoggerInterface /** * System is unusable. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function emergency(string|\Stringable $message, array $context = []); + public function emergency(string|\Stringable $message, array $context = []): void; /** * Action must be taken immediately. @@ -35,35 +32,26 @@ public function emergency(string|\Stringable $message, array $context = []); * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function alert(string|\Stringable $message, array $context = []); + public function alert(string|\Stringable $message, array $context = []): void; /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function critical(string|\Stringable $message, array $context = []); + public function critical(string|\Stringable $message, array $context = []): void; /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function error(string|\Stringable $message, array $context = []); + public function error(string|\Stringable $message, array $context = []): void; /** * Exceptional occurrences that are not errors. @@ -71,55 +59,40 @@ public function error(string|\Stringable $message, array $context = []); * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function warning(string|\Stringable $message, array $context = []); + public function warning(string|\Stringable $message, array $context = []): void; /** * Normal but significant events. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function notice(string|\Stringable $message, array $context = []); + public function notice(string|\Stringable $message, array $context = []): void; /** * Interesting events. * * Example: User logs in, SQL logs. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function info(string|\Stringable $message, array $context = []); + public function info(string|\Stringable $message, array $context = []): void; /** * Detailed debug information. * - * @param string|\Stringable $message * @param mixed[] $context - * - * @return void */ - public function debug(string|\Stringable $message, array $context = []); + public function debug(string|\Stringable $message, array $context = []): void; /** * Logs with an arbitrary level. * - * @param mixed $level - * @param string|\Stringable $message + * @param mixed $level * @param mixed[] $context * - * @return void - * * @throws \Psr\Log\InvalidArgumentException */ - public function log($level, string|\Stringable $message, array $context = []); + public function log($level, string|\Stringable $message, array $context = []): void; } diff --git a/psr/log/src/LoggerTrait.php b/psr/log/src/LoggerTrait.php index 920bda77f..a5d9980b6 100644 --- a/psr/log/src/LoggerTrait.php +++ b/psr/log/src/LoggerTrait.php @@ -14,13 +14,8 @@ trait LoggerTrait { /** * System is unusable. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function emergency(string|\Stringable $message, array $context = []) + public function emergency(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } @@ -30,13 +25,8 @@ public function emergency(string|\Stringable $message, array $context = []) * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function alert(string|\Stringable $message, array $context = []) + public function alert(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } @@ -45,13 +35,8 @@ public function alert(string|\Stringable $message, array $context = []) * Critical conditions. * * Example: Application component unavailable, unexpected exception. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function critical(string|\Stringable $message, array $context = []) + public function critical(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } @@ -59,13 +44,8 @@ public function critical(string|\Stringable $message, array $context = []) /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function error(string|\Stringable $message, array $context = []) + public function error(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } @@ -75,26 +55,16 @@ public function error(string|\Stringable $message, array $context = []) * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function warning(string|\Stringable $message, array $context = []) + public function warning(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function notice(string|\Stringable $message, array $context = []) + public function notice(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } @@ -103,26 +73,16 @@ public function notice(string|\Stringable $message, array $context = []) * Interesting events. * * Example: User logs in, SQL logs. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function info(string|\Stringable $message, array $context = []) + public function info(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. - * - * @param string|\Stringable $message - * @param array $context - * - * @return void */ - public function debug(string|\Stringable $message, array $context = []) + public function debug(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } @@ -130,13 +90,9 @@ public function debug(string|\Stringable $message, array $context = []) /** * Logs with an arbitrary level. * - * @param mixed $level - * @param string|\Stringable $message - * @param array $context - * - * @return void + * @param mixed $level * * @throws \Psr\Log\InvalidArgumentException */ - abstract public function log($level, string|\Stringable $message, array $context = []); + abstract public function log($level, string|\Stringable $message, array $context = []): void; } diff --git a/psr/log/src/NullLogger.php b/psr/log/src/NullLogger.php index 560770571..de0561e2a 100644 --- a/psr/log/src/NullLogger.php +++ b/psr/log/src/NullLogger.php @@ -15,15 +15,11 @@ class NullLogger extends AbstractLogger /** * Logs with an arbitrary level. * - * @param mixed $level - * @param string|\Stringable $message - * @param array $context - * - * @return void + * @param mixed[] $context * * @throws \Psr\Log\InvalidArgumentException */ - public function log($level, string|\Stringable $message, array $context = []) + public function log($level, string|\Stringable $message, array $context = []): void { // noop } diff --git a/stecman/symfony-console-completion/src/CompletionCommand.php b/stecman/symfony-console-completion/src/CompletionCommand.php index f694c41bd..ef0b3e486 100644 --- a/stecman/symfony-console-completion/src/CompletionCommand.php +++ b/stecman/symfony-console-completion/src/CompletionCommand.php @@ -34,16 +34,13 @@ protected function configure() ); // Hide this command from listing if supported - // Command::setHidden() was not available before Symfony 3.2.0 - if (method_exists($this, 'setHidden')) { - $this->setHidden(true); - } + $this->setHidden(true); } /** * {@inheritdoc} */ - public function getNativeDefinition() + public function getNativeDefinition(): InputDefinition { return $this->createDefinition(); } @@ -54,7 +51,7 @@ public function getNativeDefinition() * Any global options defined by user-code are meaningless to this command. * Options outside of the core defaults are ignored to avoid name and shortcut conflicts. */ - public function mergeApplicationDefinition($mergeArgs = true) + public function mergeApplicationDefinition(bool $mergeArgs = true): void { // Get current application options $appDefinition = $this->getApplication()->getDefinition(); @@ -94,7 +91,7 @@ protected function filterApplicationOptions(array $appOptions) }); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->handler = new CompletionHandler($this->getApplication()); $handler = $this->handler; @@ -135,7 +132,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->write($results, true); } - return 0; + return SymfonyCommand::SUCCESS; } /** diff --git a/stecman/symfony-console-completion/src/CompletionHandler.php b/stecman/symfony-console-completion/src/CompletionHandler.php index abd0b0bf5..236687bb3 100644 --- a/stecman/symfony-console-completion/src/CompletionHandler.php +++ b/stecman/symfony-console-completion/src/CompletionHandler.php @@ -451,28 +451,15 @@ protected function getAllOptions() */ protected function getCommandNames() { - // Command::Hidden isn't supported before Symfony Console 3.2.0 - // We don't complete hidden command names as these are intended to be private - if (method_exists('\Symfony\Component\Console\Command\Command', 'isHidden')) { - $commands = array(); - - foreach ($this->application->all() as $name => $command) { - if (!$command->isHidden()) { - $commands[] = $name; - } - } - - return $commands; - - } else { - - // Fallback for compatibility with Symfony Console < 3.2.0 - // This was the behaviour prior to pull #75 - $commands = $this->application->all(); - unset($commands['_completion']); + $commands = array(); - return array_keys($commands); + foreach ($this->application->all() as $name => $command) { + if (!$command->isHidden()) { + $commands[] = $name; + } } + + return $commands; } /** diff --git a/symfony/console/Application.php b/symfony/console/Application.php index bb5341882..b97d0872f 100644 --- a/symfony/console/Application.php +++ b/symfony/console/Application.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleSignalEvent; @@ -32,6 +33,7 @@ use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\HelperSet; @@ -70,23 +72,24 @@ */ class Application implements ResetInterface { - private $commands = []; - private $wantHelps = false; - private $runningCommand; - private $name; - private $version; - private $commandLoader; - private $catchExceptions = true; - private $autoExit = true; - private $definition; - private $helperSet; - private $dispatcher; - private $terminal; - private $defaultCommand; - private $singleCommand = false; - private $initialized; - private $signalRegistry; - private $signalsToDispatchEvent = []; + private array $commands = []; + private bool $wantHelps = false; + private ?Command $runningCommand = null; + private string $name; + private string $version; + private ?CommandLoaderInterface $commandLoader = null; + private bool $catchExceptions = true; + private bool $catchErrors = false; + private bool $autoExit = true; + private InputDefinition $definition; + private HelperSet $helperSet; + private ?EventDispatcherInterface $dispatcher = null; + private Terminal $terminal; + private string $defaultCommand; + private bool $singleCommand = false; + private bool $initialized = false; + private ?SignalRegistry $signalRegistry = null; + private array $signalsToDispatchEvent = []; public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') { @@ -103,11 +106,14 @@ public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN /** * @final */ - public function setDispatcher(EventDispatcherInterface $dispatcher) + public function setDispatcher(EventDispatcherInterface $dispatcher): void { $this->dispatcher = $dispatcher; } + /** + * @return void + */ public function setCommandLoader(CommandLoaderInterface $commandLoader) { $this->commandLoader = $commandLoader; @@ -116,12 +122,15 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader) public function getSignalRegistry(): SignalRegistry { if (!$this->signalRegistry) { - throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + throw new RuntimeException('Signals are not supported. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); } return $this->signalRegistry; } + /** + * @return void + */ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) { $this->signalsToDispatchEvent = $signalsToDispatchEvent; @@ -134,20 +143,15 @@ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) * * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}. */ - public function run(?InputInterface $input = null, ?OutputInterface $output = null) + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { if (\function_exists('putenv')) { @putenv('LINES='.$this->terminal->getHeight()); @putenv('COLUMNS='.$this->terminal->getWidth()); } - if (null === $input) { - $input = new ArgvInput(); - } - - if (null === $output) { - $output = new ConsoleOutput(); - } + $input ??= new ArgvInput(); + $output ??= new ConsoleOutput(); $renderException = function (\Throwable $e) use ($output) { if ($output instanceof ConsoleOutputInterface) { @@ -169,8 +173,11 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu try { $exitCode = $this->doRun($input, $output); - } catch (\Exception $e) { - if (!$this->catchExceptions) { + } catch (\Throwable $e) { + if ($e instanceof \Exception && !$this->catchExceptions) { + throw $e; + } + if (!$e instanceof \Exception && !$this->catchErrors) { throw $e; } @@ -228,7 +235,7 @@ public function doRun(InputInterface $input, OutputInterface $output) try { // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. $input->bind($this->getDefinition()); - } catch (ExceptionInterface $e) { + } catch (ExceptionInterface) { // Errors must be ignored, full binding/validation happens later when the command is known. } @@ -258,7 +265,26 @@ public function doRun(InputInterface $input, OutputInterface $output) // the command name MUST be the first element of the input $command = $this->find($name); } catch (\Throwable $e) { - if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) { + if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) { + $alternative = $alternatives[0]; + + $style = new SymfonyStyle($input, $output); + $output->writeln(''); + $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true); + $output->writeln($formattedBlock); + if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + return $event->getExitCode(); + } + + return 1; + } + + $command = $this->find($alternative); + } else { if (null !== $this->dispatcher) { $event = new ConsoleErrorEvent($input, $output, $e); $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); @@ -270,27 +296,24 @@ public function doRun(InputInterface $input, OutputInterface $output) $e = $event->getError(); } - throw $e; - } - - $alternative = $alternatives[0]; - - $style = new SymfonyStyle($input, $output); - $output->writeln(''); - $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true); - $output->writeln($formattedBlock); - if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { - if (null !== $this->dispatcher) { - $event = new ConsoleErrorEvent($input, $output, $e); - $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + try { + if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) { + $helper = new DescriptorHelper(); + $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [ + 'format' => 'txt', + 'raw_text' => false, + 'namespace' => $namespace, + 'short' => false, + ]); + + return isset($event) ? $event->getExitCode() : 1; + } - return $event->getExitCode(); + throw $e; + } catch (NamespaceNotFoundException) { + throw $e; } - - return 1; } - - $command = $this->find($alternative); } if ($command instanceof LazyCommand) { @@ -305,12 +328,15 @@ public function doRun(InputInterface $input, OutputInterface $output) } /** - * {@inheritdoc} + * @return void */ public function reset() { } + /** + * @return void + */ public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; @@ -318,18 +344,15 @@ public function setHelperSet(HelperSet $helperSet) /** * Get the helper set associated with the command. - * - * @return HelperSet */ - public function getHelperSet() + public function getHelperSet(): HelperSet { - if (!$this->helperSet) { - $this->helperSet = $this->getDefaultHelperSet(); - } - - return $this->helperSet; + return $this->helperSet ??= $this->getDefaultHelperSet(); } + /** + * @return void + */ public function setDefinition(InputDefinition $definition) { $this->definition = $definition; @@ -337,14 +360,10 @@ public function setDefinition(InputDefinition $definition) /** * Gets the InputDefinition related to this Application. - * - * @return InputDefinition */ - public function getDefinition() + public function getDefinition(): InputDefinition { - if (!$this->definition) { - $this->definition = $this->getDefaultInputDefinition(); - } + $this->definition ??= $this->getDefaultInputDefinition(); if ($this->singleCommand) { $inputDefinition = $this->definition; @@ -365,18 +384,16 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && 'command' === $input->getCompletionName() ) { - $commandNames = []; foreach ($this->all() as $name => $command) { // skip hidden commands and aliased commands as they already get added below if ($command->isHidden() || $command->getName() !== $name) { continue; } - $commandNames[] = $command->getName(); + $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription())); foreach ($command->getAliases() as $name) { - $commandNames[] = $name; + $suggestions->suggestValue(new Suggestion($name, $command->getDescription())); } } - $suggestions->suggestValues(array_filter($commandNames)); return; } @@ -390,44 +407,50 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti /** * Gets the help message. - * - * @return string */ - public function getHelp() + public function getHelp(): string { return $this->getLongVersion(); } /** * Gets whether to catch exceptions or not during commands execution. - * - * @return bool */ - public function areExceptionsCaught() + public function areExceptionsCaught(): bool { return $this->catchExceptions; } /** * Sets whether to catch exceptions or not during commands execution. + * + * @return void */ public function setCatchExceptions(bool $boolean) { $this->catchExceptions = $boolean; } + /** + * Sets whether to catch errors or not during commands execution. + */ + public function setCatchErrors(bool $catchErrors = true): void + { + $this->catchErrors = $catchErrors; + } + /** * Gets whether to automatically exit after a command execution or not. - * - * @return bool */ - public function isAutoExitEnabled() + public function isAutoExitEnabled(): bool { return $this->autoExit; } /** * Sets whether to automatically exit after a command execution or not. + * + * @return void */ public function setAutoExit(bool $boolean) { @@ -436,17 +459,17 @@ public function setAutoExit(bool $boolean) /** * Gets the name of the application. - * - * @return string */ - public function getName() + public function getName(): string { return $this->name; } /** * Sets the application name. - **/ + * + * @return void + */ public function setName(string $name) { $this->name = $name; @@ -454,16 +477,16 @@ public function setName(string $name) /** * Gets the application version. - * - * @return string */ - public function getVersion() + public function getVersion(): string { return $this->version; } /** * Sets the application version. + * + * @return void */ public function setVersion(string $version) { @@ -490,10 +513,8 @@ public function getLongVersion() /** * Registers a new command. - * - * @return Command */ - public function register(string $name) + public function register(string $name): Command { return $this->add(new Command($name)); } @@ -504,6 +525,8 @@ public function register(string $name) * If a Command is not enabled it will not be added. * * @param Command[] $commands An array of commands + * + * @return void */ public function addCommands(array $commands) { @@ -586,14 +609,12 @@ public function get(string $name) /** * Returns true if the command exists, false otherwise. - * - * @return bool */ - public function has(string $name) + public function has(string $name): bool { $this->init(); - return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name))); + return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name))); } /** @@ -603,7 +624,7 @@ public function has(string $name) * * @return string[] */ - public function getNamespaces() + public function getNamespaces(): array { $namespaces = []; foreach ($this->all() as $command) { @@ -624,11 +645,9 @@ public function getNamespaces() /** * Finds a registered namespace by a name or an abbreviation. * - * @return string - * * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous */ - public function findNamespace(string $namespace) + public function findNamespace(string $namespace): string { $allNamespaces = $this->getNamespaces(); $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; @@ -705,9 +724,7 @@ public function find(string $name) if ($alternatives = $this->findAlternatives($name, $allCommands)) { // remove hidden commands - $alternatives = array_filter($alternatives, function ($name) { - return !$this->get($name)->isHidden(); - }); + $alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden()); if (1 == \count($alternatives)) { $message .= "\n\nDid you mean this?\n "; @@ -820,7 +837,7 @@ public function all(?string $namespace = null) * * @return string[][] */ - public static function getAbbreviations(array $names) + public static function getAbbreviations(array $names): array { $abbrevs = []; foreach ($names as $name) { @@ -858,9 +875,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo } if (str_contains($message, "@anonymous\0")) { - $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { - return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; - }, $message); + $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message); } $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX; @@ -921,6 +936,8 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo /** * Configures the input and output instances based on the user arguments and options. + * + * @return void */ protected function configureIO(InputInterface $input, OutputInterface $output) { @@ -995,44 +1012,65 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } } - if ($this->signalsToDispatchEvent) { - $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; - - if ($commandSignals || null !== $this->dispatcher) { - if (!$this->signalRegistry) { - throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); - } + $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; + if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) { + if (!$this->signalRegistry) { + throw new RuntimeException('Unable to subscribe to signal events. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } - if (Terminal::hasSttyAvailable()) { - $sttyMode = shell_exec('stty -g'); + if (Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); - foreach ([\SIGINT, \SIGTERM] as $signal) { - $this->signalRegistry->register($signal, static function () use ($sttyMode) { - shell_exec('stty '.$sttyMode); - }); - } + foreach ([\SIGINT, \SIGTERM] as $signal) { + $this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode)); } } - if (null !== $this->dispatcher) { + if ($this->dispatcher) { + // We register application signals, so that we can dispatch the event foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); - $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) { + $this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) { $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); - - // No more handlers, we try to simulate PHP default behavior - if (!$hasNext) { - if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) { - exit(0); + $exitCode = $event->getExitCode(); + + // If the command is signalable, we call the handleSignal() method + if (\in_array($signal, $commandSignals, true)) { + $exitCode = $command->handleSignal($signal, $exitCode); + // BC layer for Symfony <= 5 + if (null === $exitCode) { + trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command)); + $exitCode = 0; } } + + if (false !== $exitCode) { + $event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal); + $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); + + exit($event->getExitCode()); + } }); } + + // then we register command signals, but not if already handled after the dispatcher + $commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent); } foreach ($commandSignals as $signal) { - $this->signalRegistry->register($signal, [$command, 'handleSignal']); + $this->signalRegistry->register($signal, function (int $signal) use ($command): void { + $exitCode = $command->handleSignal($signal); + // BC layer for Symfony <= 5 + if (null === $exitCode) { + trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command)); + $exitCode = 0; + } + + if (false !== $exitCode) { + exit($exitCode); + } + }); } } @@ -1044,7 +1082,7 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI try { $command->mergeApplicationDefinition(); $input->bind($command->getDefinition()); - } catch (ExceptionInterface $e) { + } catch (ExceptionInterface) { // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition } @@ -1081,20 +1119,16 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI /** * Gets the name of the command based on input. - * - * @return string|null */ - protected function getCommandName(InputInterface $input) + protected function getCommandName(InputInterface $input): ?string { return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); } /** * Gets the default input definition. - * - * @return InputDefinition */ - protected function getDefaultInputDefinition() + protected function getDefaultInputDefinition(): InputDefinition { return new InputDefinition([ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), @@ -1112,17 +1146,15 @@ protected function getDefaultInputDefinition() * * @return Command[] */ - protected function getDefaultCommands() + protected function getDefaultCommands(): array { return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; } /** * Gets the default helper set with the helpers that should always be available. - * - * @return HelperSet */ - protected function getDefaultHelperSet() + protected function getDefaultHelperSet(): HelperSet { return new HelperSet([ new FormatterHelper(), @@ -1144,10 +1176,8 @@ private function getAbbreviationSuggestions(array $abbrevs): string * Returns the namespace part of the command name. * * This method is not part of public API and should not be used directly. - * - * @return string */ - public function extractNamespace(string $name, ?int $limit = null) + public function extractNamespace(string $name, ?int $limit = null): string { $parts = explode(':', $name, -1); @@ -1196,7 +1226,7 @@ private function findAlternatives(string $name, iterable $collection): array } } - $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + $alternatives = array_filter($alternatives, fn ($lev) => $lev < 2 * $threshold); ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); return array_keys($alternatives); @@ -1207,7 +1237,7 @@ private function findAlternatives(string $name, iterable $collection): array * * @return $this */ - public function setDefaultCommand(string $commandName, bool $isSingleCommand = false) + public function setDefaultCommand(string $commandName, bool $isSingleCommand = false): static { $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; @@ -1287,7 +1317,7 @@ private function extractAllNamespaces(string $name): array return $namespaces; } - private function init() + private function init(): void { if ($this->initialized) { return; diff --git a/symfony/console/CI/GithubActionReporter.php b/symfony/console/CI/GithubActionReporter.php index 065717854..2cae6fd8b 100644 --- a/symfony/console/CI/GithubActionReporter.php +++ b/symfony/console/CI/GithubActionReporter.php @@ -20,7 +20,7 @@ */ class GithubActionReporter { - private $output; + private OutputInterface $output; /** * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85 diff --git a/symfony/console/Color.php b/symfony/console/Color.php index 22a4ce9ff..60ed046a6 100644 --- a/symfony/console/Color.php +++ b/symfony/console/Color.php @@ -49,9 +49,9 @@ final class Color 'conceal' => ['set' => 8, 'unset' => 28], ]; - private $foreground; - private $background; - private $options = []; + private string $foreground; + private string $background; + private array $options = []; public function __construct(string $foreground = '', string $background = '', array $options = []) { @@ -117,17 +117,7 @@ private function parseColor(string $color, bool $background = false): string } if ('#' === $color[0]) { - $color = substr($color, 1); - - if (3 === \strlen($color)) { - $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; - } - - if (6 !== \strlen($color)) { - throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color)); - } - - return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color)); + return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color); } if (isset(self::COLORS[$color])) { @@ -140,41 +130,4 @@ private function parseColor(string $color, bool $background = false): string throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS))))); } - - private function convertHexColorToAnsi(int $color): string - { - $r = ($color >> 16) & 255; - $g = ($color >> 8) & 255; - $b = $color & 255; - - // see https://github.com/termstandard/colors/ for more information about true color support - if ('truecolor' !== getenv('COLORTERM')) { - return (string) $this->degradeHexColorToAnsi($r, $g, $b); - } - - return sprintf('8;2;%d;%d;%d', $r, $g, $b); - } - - private function degradeHexColorToAnsi(int $r, int $g, int $b): int - { - if (0 === round($this->getSaturation($r, $g, $b) / 50)) { - return 0; - } - - return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255); - } - - private function getSaturation(int $r, int $g, int $b): int - { - $r = $r / 255; - $g = $g / 255; - $b = $b / 255; - $v = max($r, $g, $b); - - if (0 === $diff = $v - min($r, $g, $b)) { - return 0; - } - - return (int) $diff * 100 / $v; - } } diff --git a/symfony/console/Command/Command.php b/symfony/console/Command/Command.php index d18103670..9f9cb2f53 100644 --- a/symfony/console/Command/Command.php +++ b/symfony/console/Command/Command.php @@ -15,9 +15,11 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Helper\HelperInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; @@ -39,56 +41,69 @@ class Command /** * @var string|null The default command name + * + * @deprecated since Symfony 6.1, use the AsCommand attribute instead */ protected static $defaultName; /** * @var string|null The default command description + * + * @deprecated since Symfony 6.1, use the AsCommand attribute instead */ protected static $defaultDescription; - private $application; - private $name; - private $processTitle; - private $aliases = []; - private $definition; - private $hidden = false; - private $help = ''; - private $description = ''; - private $fullDefinition; - private $ignoreValidationErrors = false; - private $code; - private $synopsis = []; - private $usages = []; - private $helperSet; - - /** - * @return string|null - */ - public static function getDefaultName() + private ?Application $application = null; + private ?string $name = null; + private ?string $processTitle = null; + private array $aliases = []; + private InputDefinition $definition; + private bool $hidden = false; + private string $help = ''; + private string $description = ''; + private ?InputDefinition $fullDefinition = null; + private bool $ignoreValidationErrors = false; + private ?\Closure $code = null; + private array $synopsis = []; + private array $usages = []; + private ?HelperSet $helperSet = null; + + public static function getDefaultName(): ?string { $class = static::class; - if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->name; } $r = new \ReflectionProperty($class, 'defaultName'); - return $class === $r->class ? static::$defaultName : null; + if ($class !== $r->class || null === static::$defaultName) { + return null; + } + + trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); + + return static::$defaultName; } public static function getDefaultDescription(): ?string { $class = static::class; - if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->description; } $r = new \ReflectionProperty($class, 'defaultDescription'); - return $class === $r->class ? static::$defaultDescription : null; + if ($class !== $r->class || null === static::$defaultDescription) { + return null; + } + + trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); + + return static::$defaultDescription; } /** @@ -126,14 +141,22 @@ public function __construct(?string $name = null) * Ignores validation errors. * * This is mainly useful for the help command. + * + * @return void */ public function ignoreValidationErrors() { $this->ignoreValidationErrors = true; } + /** + * @return void + */ public function setApplication(?Application $application = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); @@ -144,6 +167,9 @@ public function setApplication(?Application $application = null) $this->fullDefinition = null; } + /** + * @return void + */ public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; @@ -151,20 +177,16 @@ public function setHelperSet(HelperSet $helperSet) /** * Gets the helper set. - * - * @return HelperSet|null */ - public function getHelperSet() + public function getHelperSet(): ?HelperSet { return $this->helperSet; } /** * Gets the application instance for this command. - * - * @return Application|null */ - public function getApplication() + public function getApplication(): ?Application { return $this->application; } @@ -184,6 +206,8 @@ public function isEnabled() /** * Configures the current command. + * + * @return void */ protected function configure() { @@ -214,6 +238,8 @@ protected function execute(InputInterface $input, OutputInterface $output) * This method is executed before the InputDefinition is validated. * This means that this is the only place where the command can * interactively ask for values of missing required arguments. + * + * @return void */ protected function interact(InputInterface $input, OutputInterface $output) { @@ -228,6 +254,8 @@ protected function interact(InputInterface $input, OutputInterface $output) * * @see InputInterface::bind() * @see InputInterface::validate() + * + * @return void */ protected function initialize(InputInterface $input, OutputInterface $output) { @@ -247,7 +275,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) * @see setCode() * @see execute() */ - public function run(InputInterface $input, OutputInterface $output) + public function run(InputInterface $input, OutputInterface $output): int { // add the application arguments and options $this->mergeApplicationDefinition(); @@ -310,6 +338,12 @@ public function run(InputInterface $input, OutputInterface $output) */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { + $definition = $this->getDefinition(); + if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) { + $definition->getOption($input->getCompletionName())->complete($input, $suggestions); + } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) { + $definition->getArgument($input->getCompletionName())->complete($input, $suggestions); + } } /** @@ -326,7 +360,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti * * @see execute() */ - public function setCode(callable $code) + public function setCode(callable $code): static { if ($code instanceof \Closure) { $r = new \ReflectionFunction($code); @@ -340,6 +374,8 @@ public function setCode(callable $code) restore_error_handler(); } } + } else { + $code = $code(...); } $this->code = $code; @@ -356,7 +392,7 @@ public function setCode(callable $code) * * @internal */ - public function mergeApplicationDefinition(bool $mergeArgs = true) + public function mergeApplicationDefinition(bool $mergeArgs = true): void { if (null === $this->application) { return; @@ -377,11 +413,9 @@ public function mergeApplicationDefinition(bool $mergeArgs = true) /** * Sets an array of argument and option instances. * - * @param array|InputDefinition $definition An array of argument and option instances or a definition instance - * * @return $this */ - public function setDefinition($definition) + public function setDefinition(array|InputDefinition $definition): static { if ($definition instanceof InputDefinition) { $this->definition = $definition; @@ -396,10 +430,8 @@ public function setDefinition($definition) /** * Gets the InputDefinition attached to this Command. - * - * @return InputDefinition */ - public function getDefinition() + public function getDefinition(): InputDefinition { return $this->fullDefinition ?? $this->getNativeDefinition(); } @@ -411,34 +443,31 @@ public function getDefinition() * be changed by merging with the application InputDefinition. * * This method is not part of public API and should not be used directly. - * - * @return InputDefinition */ - public function getNativeDefinition() + public function getNativeDefinition(): InputDefinition { - if (null === $this->definition) { - throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); - } - - return $this->definition; + return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); } /** * Adds an argument. * - * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL - * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) + * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param $default The default value (for InputArgument::OPTIONAL mode only) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @return $this * * @throws InvalidArgumentException When argument mode is not valid */ - public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null) + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static { - $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); - if (null !== $this->fullDefinition) { - $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default)); + $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; + if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { + throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); } + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); + $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); return $this; } @@ -446,20 +475,23 @@ public function addArgument(string $name, ?int $mode = null, string $description /** * Adds an option. * - * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts - * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants - * @param mixed $default The default value (must be null for InputOption::VALUE_NONE) + * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param $mode The option mode: One of the InputOption::VALUE_* constants + * @param $default The default value (must be null for InputOption::VALUE_NONE) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @return $this * * @throws InvalidArgumentException If option mode is invalid or incompatible */ - public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null) + public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { - $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); - if (null !== $this->fullDefinition) { - $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; + if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { + throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); } + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); + $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); return $this; } @@ -476,7 +508,7 @@ public function addOption(string $name, $shortcut = null, ?int $mode = null, str * * @throws InvalidArgumentException When the name is invalid */ - public function setName(string $name) + public function setName(string $name): static { $this->validateName($name); @@ -493,7 +525,7 @@ public function setName(string $name) * * @return $this */ - public function setProcessTitle(string $title) + public function setProcessTitle(string $title): static { $this->processTitle = $title; @@ -502,23 +534,18 @@ public function setProcessTitle(string $title) /** * Returns the command name. - * - * @return string|null */ - public function getName() + public function getName(): ?string { return $this->name; } /** * @param bool $hidden Whether or not the command should be hidden from the list of commands - * The default value will be true in Symfony 6.0 * * @return $this - * - * @final since Symfony 5.1 */ - public function setHidden(bool $hidden /* = true */) + public function setHidden(bool $hidden = true): static { $this->hidden = $hidden; @@ -528,7 +555,7 @@ public function setHidden(bool $hidden /* = true */) /** * @return bool whether the command should be publicly shown or not */ - public function isHidden() + public function isHidden(): bool { return $this->hidden; } @@ -538,7 +565,7 @@ public function isHidden() * * @return $this */ - public function setDescription(string $description) + public function setDescription(string $description): static { $this->description = $description; @@ -547,10 +574,8 @@ public function setDescription(string $description) /** * Returns the description for the command. - * - * @return string */ - public function getDescription() + public function getDescription(): string { return $this->description; } @@ -560,7 +585,7 @@ public function getDescription() * * @return $this */ - public function setHelp(string $help) + public function setHelp(string $help): static { $this->help = $help; @@ -569,10 +594,8 @@ public function setHelp(string $help) /** * Returns the help for the command. - * - * @return string */ - public function getHelp() + public function getHelp(): string { return $this->help; } @@ -580,13 +603,11 @@ public function getHelp() /** * Returns the processed help for the command replacing the %command.name% and * %command.full_name% patterns with the real values dynamically. - * - * @return string */ - public function getProcessedHelp() + public function getProcessedHelp(): string { $name = $this->name; - $isSingleCommand = $this->application && $this->application->isSingleCommand(); + $isSingleCommand = $this->application?->isSingleCommand(); $placeholders = [ '%command.name%', @@ -609,7 +630,7 @@ public function getProcessedHelp() * * @throws InvalidArgumentException When an alias is invalid */ - public function setAliases(iterable $aliases) + public function setAliases(iterable $aliases): static { $list = []; @@ -625,10 +646,8 @@ public function setAliases(iterable $aliases) /** * Returns the aliases for the command. - * - * @return array */ - public function getAliases() + public function getAliases(): array { return $this->aliases; } @@ -637,10 +656,8 @@ public function getAliases() * Returns the synopsis for the command. * * @param bool $short Whether to show the short version of the synopsis (with options folded) or not - * - * @return string */ - public function getSynopsis(bool $short = false) + public function getSynopsis(bool $short = false): string { $key = $short ? 'short' : 'long'; @@ -656,7 +673,7 @@ public function getSynopsis(bool $short = false) * * @return $this */ - public function addUsage(string $usage) + public function addUsage(string $usage): static { if (!str_starts_with($usage, $this->name)) { $usage = sprintf('%s %s', $this->name, $usage); @@ -669,10 +686,8 @@ public function addUsage(string $usage) /** * Returns alternative usages of the command. - * - * @return array */ - public function getUsages() + public function getUsages(): array { return $this->usages; } @@ -680,12 +695,12 @@ public function getUsages() /** * Gets a helper instance by name. * - * @return mixed + * @return HelperInterface * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ - public function getHelper(string $name) + public function getHelper(string $name): mixed { if (null === $this->helperSet) { throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name)); @@ -701,7 +716,7 @@ public function getHelper(string $name) * * @throws InvalidArgumentException When the name is invalid */ - private function validateName(string $name) + private function validateName(string $name): void { if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); diff --git a/symfony/console/Command/CompleteCommand.php b/symfony/console/Command/CompleteCommand.php index 0e35143c3..23be5577b 100644 --- a/symfony/console/Command/CompleteCommand.php +++ b/symfony/console/Command/CompleteCommand.php @@ -11,10 +11,13 @@ namespace Symfony\Component\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Output\BashCompletionOutput; use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Completion\Output\FishCompletionOutput; +use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputInterface; @@ -26,14 +29,24 @@ * * @author Wouter de Jong */ +#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')] final class CompleteCommand extends Command { + public const COMPLETION_API_VERSION = '1'; + + /** + * @deprecated since Symfony 6.1 + */ protected static $defaultName = '|_complete'; + + /** + * @deprecated since Symfony 6.1 + */ protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; - private $completionOutputs; + private array $completionOutputs; - private $isDebug = false; + private bool $isDebug = false; /** * @param array> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value @@ -41,7 +54,11 @@ final class CompleteCommand extends Command public function __construct(array $completionOutputs = []) { // must be set before the parent constructor, as the property value is used in configure() - $this->completionOutputs = $completionOutputs + ['bash' => BashCompletionOutput::class]; + $this->completionOutputs = $completionOutputs + [ + 'bash' => BashCompletionOutput::class, + 'fish' => FishCompletionOutput::class, + 'zsh' => ZshCompletionOutput::class, + ]; parent::__construct(); } @@ -52,28 +69,29 @@ protected function configure(): void ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")') ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') - ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script') + ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script') + ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated') ; } - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { - $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN); + $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL); } protected function execute(InputInterface $input, OutputInterface $output): int { try { - // uncomment when a bugfix or BC break has been introduced in the shell completion scripts - // $version = $input->getOption('symfony'); - // if ($version && version_compare($version, 'x.y', '>=')) { - // $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version); - // $this->log($message); + // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1 + $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version'); + if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) { + $message = sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION); + $this->log($message); - // $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); + $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); - // return 126; - // } + return 126; + } $shell = $input->getOption('shell'); if (!$shell) { @@ -116,12 +134,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $completionInput->bind($command->getDefinition()); if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) { - $this->log(' Completing option names for the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' command.'); + $this->log(' Completing option names for the '.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.' command.'); $suggestions->suggestOptions($command->getDefinition()->getOptions()); } else { $this->log([ - ' Completing using the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' class.', + ' Completing using the '.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.' class.', ' Completing '.$completionInput->getCompletionType().' for '.$completionInput->getCompletionName().'', ]); if (null !== $compval = $completionInput->getCompletionValue()) { @@ -137,7 +155,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->log('Suggestions:'); if ($options = $suggestions->getOptionSuggestions()) { - $this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options))); + $this->log(' --'.implode(' --', array_map(fn ($o) => $o->getName(), $options))); } elseif ($values = $suggestions->getValueSuggestions()) { $this->log(' '.implode(' ', $values)); } else { @@ -172,7 +190,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput try { $completionInput->bind($this->getApplication()->getDefinition()); - } catch (ExceptionInterface $e) { + } catch (ExceptionInterface) { } return $completionInput; @@ -187,7 +205,7 @@ private function findCommand(CompletionInput $completionInput, OutputInterface $ } return $this->getApplication()->find($inputName); - } catch (CommandNotFoundException $e) { + } catch (CommandNotFoundException) { } return null; diff --git a/symfony/console/Command/DumpCompletionCommand.php b/symfony/console/Command/DumpCompletionCommand.php index eaf22be1a..51b613a14 100644 --- a/symfony/console/Command/DumpCompletionCommand.php +++ b/symfony/console/Command/DumpCompletionCommand.php @@ -11,8 +11,7 @@ namespace Symfony\Component\Console\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -25,55 +24,67 @@ * * @author Wouter de Jong */ +#[AsCommand(name: 'completion', description: 'Dump the shell completion script')] final class DumpCompletionCommand extends Command { + /** + * @deprecated since Symfony 6.1 + */ protected static $defaultName = 'completion'; + + /** + * @deprecated since Symfony 6.1 + */ protected static $defaultDescription = 'Dump the shell completion script'; - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('shell')) { - $suggestions->suggestValues($this->getSupportedShells()); - } - } + private array $supportedShells; - protected function configure() + protected function configure(): void { $fullCommand = $_SERVER['PHP_SELF']; $commandName = basename($fullCommand); $fullCommand = @realpath($fullCommand) ?: $fullCommand; + $shell = $this->guessShell(); + [$rcFile, $completionFile] = match ($shell) { + 'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"], + 'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName], + default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"], + }; + + $supportedShells = implode(', ', $this->getSupportedShells()); + $this ->setHelp(<<%command.name% command dumps the shell completion script required -to use shell autocompletion (currently only bash completion is supported). +to use shell autocompletion (currently, {$supportedShells} completion are supported). Static installation ------------------- Dump the script to a global completion file and restart your shell: - %command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName} + %command.full_name% {$shell} | sudo tee {$completionFile} Or dump the script to a local file and source it: - %command.full_name% bash > completion.sh + %command.full_name% {$shell} > completion.sh # source the file whenever you use the project source completion.sh - # or add this line at the end of your "~/.bashrc" file: + # or add this line at the end of your "{$rcFile}" file: source /path/to/completion.sh Dynamic installation -------------------- -Add this to the end of your shell configuration file (e.g. "~/.bashrc"): +Add this to the end of your shell configuration file (e.g. "{$rcFile}"): - eval "$({$fullCommand} completion bash)" + eval "$({$fullCommand} completion {$shell})" EOH ) - ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given') + ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...)) ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') ; } @@ -105,7 +116,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 2; } - $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile))); + $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile))); return 0; } @@ -132,6 +143,10 @@ private function tailDebugLog(string $commandName, OutputInterface $output): voi */ private function getSupportedShells(): array { + if (isset($this->supportedShells)) { + return $this->supportedShells; + } + $shells = []; foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) { @@ -139,7 +154,8 @@ private function getSupportedShells(): array $shells[] = $file->getExtension(); } } + sort($shells); - return $shells; + return $this->supportedShells = $shells; } } diff --git a/symfony/console/Command/HelpCommand.php b/symfony/console/Command/HelpCommand.php index c66ef463e..e6447b050 100644 --- a/symfony/console/Command/HelpCommand.php +++ b/symfony/console/Command/HelpCommand.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Console\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; @@ -27,10 +25,10 @@ */ class HelpCommand extends Command { - private $command; + private Command $command; /** - * {@inheritdoc} + * @return void */ protected function configure() { @@ -39,8 +37,8 @@ protected function configure() $this ->setName('help') ->setDefinition([ - new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), ]) ->setDescription('Display help for a command') @@ -59,19 +57,17 @@ protected function configure() ; } + /** + * @return void + */ public function setCommand(Command $command) { $this->command = $command; } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - if (null === $this->command) { - $this->command = $this->getApplication()->find($input->getArgument('command_name')); - } + $this->command ??= $this->getApplication()->find($input->getArgument('command_name')); $helper = new DescriptorHelper(); $helper->describe($output, $this->command, [ @@ -79,23 +75,8 @@ protected function execute(InputInterface $input, OutputInterface $output) 'raw_text' => $input->getOption('raw'), ]); - $this->command = null; + unset($this->command); return 0; } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('command_name')) { - $descriptor = new ApplicationDescription($this->getApplication()); - $suggestions->suggestValues(array_keys($descriptor->getCommands())); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); - } - } } diff --git a/symfony/console/Command/LazyCommand.php b/symfony/console/Command/LazyCommand.php index 302a0809e..b94da6665 100644 --- a/symfony/console/Command/LazyCommand.php +++ b/symfony/console/Command/LazyCommand.php @@ -14,6 +14,8 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Helper\HelperInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; @@ -24,8 +26,8 @@ */ final class LazyCommand extends Command { - private $command; - private $isEnabled; + private \Closure|Command $command; + private ?bool $isEnabled; public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true) { @@ -45,6 +47,9 @@ public function ignoreValidationErrors(): void public function setApplication(?Application $application = null): void { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if ($this->command instanceof parent) { $this->command->setApplication($application); } @@ -76,10 +81,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti $this->getCommand()->complete($input, $suggestions); } - /** - * @return $this - */ - public function setCode(callable $code): self + public function setCode(callable $code): static { $this->getCommand()->setCode($code); @@ -94,10 +96,7 @@ public function mergeApplicationDefinition(bool $mergeArgs = true): void $this->getCommand()->mergeApplicationDefinition($mergeArgs); } - /** - * @return $this - */ - public function setDefinition($definition): self + public function setDefinition(array|InputDefinition $definition): static { $this->getCommand()->setDefinition($definition); @@ -115,39 +114,35 @@ public function getNativeDefinition(): InputDefinition } /** - * @return $this + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion */ - public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null): self + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { - $this->getCommand()->addArgument($name, $mode, $description, $default); + $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; + $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues); return $this; } /** - * @return $this + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion */ - public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): self + public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { - $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default); + $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; + $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); return $this; } - /** - * @return $this - */ - public function setProcessTitle(string $title): self + public function setProcessTitle(string $title): static { $this->getCommand()->setProcessTitle($title); return $this; } - /** - * @return $this - */ - public function setHelp(string $help): self + public function setHelp(string $help): static { $this->getCommand()->setHelp($help); @@ -169,10 +164,7 @@ public function getSynopsis(bool $short = false): string return $this->getCommand()->getSynopsis($short); } - /** - * @return $this - */ - public function addUsage(string $usage): self + public function addUsage(string $usage): static { $this->getCommand()->addUsage($usage); @@ -184,10 +176,7 @@ public function getUsages(): array return $this->getCommand()->getUsages(); } - /** - * @return mixed - */ - public function getHelper(string $name) + public function getHelper(string $name): HelperInterface { return $this->getCommand()->getHelper($name); } diff --git a/symfony/console/Command/ListCommand.php b/symfony/console/Command/ListCommand.php index f04a4ef67..5850c3d7b 100644 --- a/symfony/console/Command/ListCommand.php +++ b/symfony/console/Command/ListCommand.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Console\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; @@ -28,16 +26,16 @@ class ListCommand extends Command { /** - * {@inheritdoc} + * @return void */ protected function configure() { $this ->setName('list') ->setDefinition([ - new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), ]) ->setDescription('List commands') @@ -62,10 +60,7 @@ protected function configure() ; } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $helper = new DescriptorHelper(); $helper->describe($output, $this->getApplication(), [ @@ -77,19 +72,4 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('namespace')) { - $descriptor = new ApplicationDescription($this->getApplication()); - $suggestions->suggestValues(array_keys($descriptor->getNamespaces())); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); - } - } } diff --git a/symfony/console/Command/LockableTrait.php b/symfony/console/Command/LockableTrait.php index d21edc2c0..cd7548f02 100644 --- a/symfony/console/Command/LockableTrait.php +++ b/symfony/console/Command/LockableTrait.php @@ -24,8 +24,7 @@ */ trait LockableTrait { - /** @var LockInterface|null */ - private $lock; + private ?LockInterface $lock = null; /** * Locks a command. @@ -33,7 +32,7 @@ trait LockableTrait private function lock(?string $name = null, bool $blocking = false): bool { if (!class_exists(SemaphoreStore::class)) { - throw new LogicException('To enable the locking feature you must install the symfony/lock component.'); + throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".'); } if (null !== $this->lock) { @@ -59,7 +58,7 @@ private function lock(?string $name = null, bool $blocking = false): bool /** * Releases the command lock if there is one. */ - private function release() + private function release(): void { if ($this->lock) { $this->lock->release(); diff --git a/symfony/console/Command/SignalableCommandInterface.php b/symfony/console/Command/SignalableCommandInterface.php index d439728b6..f8eb8e522 100644 --- a/symfony/console/Command/SignalableCommandInterface.php +++ b/symfony/console/Command/SignalableCommandInterface.php @@ -25,6 +25,10 @@ public function getSubscribedSignals(): array; /** * The method will be called when the application is signaled. + * + * @param int|false $previousExitCode + * + * @return int|false The exit code to return or false to continue the normal execution */ - public function handleSignal(int $signal): void; + public function handleSignal(int $signal, /* int|false $previousExitCode = 0 */); } diff --git a/symfony/console/Command/TraceableCommand.php b/symfony/console/Command/TraceableCommand.php new file mode 100644 index 000000000..9ffb68da3 --- /dev/null +++ b/symfony/console/Command/TraceableCommand.php @@ -0,0 +1,356 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Helper\HelperInterface; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @internal + * + * @author Jules Pietri + */ +final class TraceableCommand extends Command implements SignalableCommandInterface +{ + public readonly Command $command; + public int $exitCode; + public ?int $interruptedBySignal = null; + public bool $ignoreValidation; + public bool $isInteractive = false; + public string $duration = 'n/a'; + public string $maxMemoryUsage = 'n/a'; + public InputInterface $input; + public OutputInterface $output; + /** @var array */ + public array $arguments; + /** @var array */ + public array $options; + /** @var array */ + public array $interactiveInputs = []; + public array $handledSignals = []; + + public function __construct( + Command $command, + private readonly Stopwatch $stopwatch, + ) { + if ($command instanceof LazyCommand) { + $command = $command->getCommand(); + } + + $this->command = $command; + + // prevent call to self::getDefaultDescription() + $this->setDescription($command->getDescription()); + + parent::__construct($command->getName()); + + // init below enables calling {@see parent::run()} + [$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(function () { + return [$this->code, $this->processTitle, $this->ignoreValidationErrors]; + }, $command, Command::class)(); + + if (\is_callable($code)) { + $this->setCode($code); + } + + if ($processTitle) { + parent::setProcessTitle($processTitle); + } + + if ($ignoreValidationErrors) { + parent::ignoreValidationErrors(); + } + + $this->ignoreValidation = $ignoreValidationErrors; + } + + public function __call(string $name, array $arguments): mixed + { + return $this->command->{$name}(...$arguments); + } + + public function getSubscribedSignals(): array + { + return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : []; + } + + public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false + { + if (!$this->command instanceof SignalableCommandInterface) { + return false; + } + + $event = $this->stopwatch->start($this->getName().'.handle_signal'); + + $exit = $this->command->handleSignal($signal, $previousExitCode); + + $event->stop(); + + if (!isset($this->handledSignals[$signal])) { + $this->handledSignals[$signal] = [ + 'handled' => 0, + 'duration' => 0, + 'memory' => 0, + ]; + } + + ++$this->handledSignals[$signal]['handled']; + $this->handledSignals[$signal]['duration'] += $event->getDuration(); + $this->handledSignals[$signal]['memory'] = max( + $this->handledSignals[$signal]['memory'], + $event->getMemory() >> 20 + ); + + return $exit; + } + + /** + * {@inheritdoc} + * + * Calling parent method is required to be used in {@see parent::run()}. + */ + public function ignoreValidationErrors(): void + { + $this->ignoreValidation = true; + $this->command->ignoreValidationErrors(); + + parent::ignoreValidationErrors(); + } + + public function setApplication(?Application $application = null): void + { + $this->command->setApplication($application); + } + + public function getApplication(): ?Application + { + return $this->command->getApplication(); + } + + public function setHelperSet(HelperSet $helperSet): void + { + $this->command->setHelperSet($helperSet); + } + + public function getHelperSet(): ?HelperSet + { + return $this->command->getHelperSet(); + } + + public function isEnabled(): bool + { + return $this->command->isEnabled(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->command->complete($input, $suggestions); + } + + /** + * {@inheritdoc} + * + * Calling parent method is required to be used in {@see parent::run()}. + */ + public function setCode(callable $code): static + { + $this->command->setCode($code); + + return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int { + $event = $this->stopwatch->start($this->getName().'.code'); + + $this->exitCode = $code($input, $output); + + $event->stop(); + + return $this->exitCode; + }); + } + + /** + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + $this->command->mergeApplicationDefinition($mergeArgs); + } + + public function setDefinition(array|InputDefinition $definition): static + { + $this->command->setDefinition($definition); + + return $this; + } + + public function getDefinition(): InputDefinition + { + return $this->command->getDefinition(); + } + + public function getNativeDefinition(): InputDefinition + { + return $this->command->getNativeDefinition(); + } + + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static + { + $this->command->addArgument($name, $mode, $description, $default, $suggestedValues); + + return $this; + } + + public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static + { + $this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); + + return $this; + } + + /** + * {@inheritdoc} + * + * Calling parent method is required to be used in {@see parent::run()}. + */ + public function setProcessTitle(string $title): static + { + $this->command->setProcessTitle($title); + + return parent::setProcessTitle($title); + } + + public function setHelp(string $help): static + { + $this->command->setHelp($help); + + return $this; + } + + public function getHelp(): string + { + return $this->command->getHelp(); + } + + public function getProcessedHelp(): string + { + return $this->command->getProcessedHelp(); + } + + public function getSynopsis(bool $short = false): string + { + return $this->command->getSynopsis($short); + } + + public function addUsage(string $usage): static + { + $this->command->addUsage($usage); + + return $this; + } + + public function getUsages(): array + { + return $this->command->getUsages(); + } + + public function getHelper(string $name): HelperInterface + { + return $this->command->getHelper($name); + } + + public function run(InputInterface $input, OutputInterface $output): int + { + $this->input = $input; + $this->output = $output; + $this->arguments = $input->getArguments(); + $this->options = $input->getOptions(); + $event = $this->stopwatch->start($this->getName(), 'command'); + + try { + $this->exitCode = parent::run($input, $output); + } finally { + $event->stop(); + + if ($output instanceof ConsoleOutputInterface && $output->isDebug()) { + $output->getErrorOutput()->writeln((string) $event); + } + + $this->duration = $event->getDuration().' ms'; + $this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB'; + + if ($this->isInteractive) { + $this->extractInteractiveInputs($input->getArguments(), $input->getOptions()); + } + } + + return $this->exitCode; + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $event = $this->stopwatch->start($this->getName().'.init', 'command'); + + $this->command->initialize($input, $output); + + $event->stop(); + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) { + return; + } + + $event = $this->stopwatch->start($this->getName().'.interact', 'command'); + + $this->command->interact($input, $output); + + $event->stop(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $event = $this->stopwatch->start($this->getName().'.execute', 'command'); + + $exitCode = $this->command->execute($input, $output); + + $event->stop(); + + return $exitCode; + } + + private function extractInteractiveInputs(array $arguments, array $options): void + { + foreach ($arguments as $argName => $argValue) { + if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) { + continue; + } + + $this->interactiveInputs[$argName] = $argValue; + } + + foreach ($options as $optName => $optValue) { + if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) { + continue; + } + + $this->interactiveInputs['--'.$optName] = $optValue; + } + } +} diff --git a/symfony/console/CommandLoader/CommandLoaderInterface.php b/symfony/console/CommandLoader/CommandLoaderInterface.php index 0adaf886f..b6b637ce6 100644 --- a/symfony/console/CommandLoader/CommandLoaderInterface.php +++ b/symfony/console/CommandLoader/CommandLoaderInterface.php @@ -22,21 +22,17 @@ interface CommandLoaderInterface /** * Loads a command. * - * @return Command - * * @throws CommandNotFoundException */ - public function get(string $name); + public function get(string $name): Command; /** * Checks if a command exists. - * - * @return bool */ - public function has(string $name); + public function has(string $name): bool; /** * @return string[] */ - public function getNames(); + public function getNames(): array; } diff --git a/symfony/console/CommandLoader/ContainerCommandLoader.php b/symfony/console/CommandLoader/ContainerCommandLoader.php index ddccb3d45..bfa0ac467 100644 --- a/symfony/console/CommandLoader/ContainerCommandLoader.php +++ b/symfony/console/CommandLoader/ContainerCommandLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\CommandLoader; use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** @@ -21,8 +22,8 @@ */ class ContainerCommandLoader implements CommandLoaderInterface { - private $container; - private $commandMap; + private ContainerInterface $container; + private array $commandMap; /** * @param array $commandMap An array with command names as keys and service ids as values @@ -33,10 +34,7 @@ public function __construct(ContainerInterface $container, array $commandMap) $this->commandMap = $commandMap; } - /** - * {@inheritdoc} - */ - public function get(string $name) + public function get(string $name): Command { if (!$this->has($name)) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); @@ -45,18 +43,12 @@ public function get(string $name) return $this->container->get($this->commandMap[$name]); } - /** - * {@inheritdoc} - */ - public function has(string $name) + public function has(string $name): bool { return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); } - /** - * {@inheritdoc} - */ - public function getNames() + public function getNames(): array { return array_keys($this->commandMap); } diff --git a/symfony/console/CommandLoader/FactoryCommandLoader.php b/symfony/console/CommandLoader/FactoryCommandLoader.php index 7e2db3464..9ced75aeb 100644 --- a/symfony/console/CommandLoader/FactoryCommandLoader.php +++ b/symfony/console/CommandLoader/FactoryCommandLoader.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\CommandLoader; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** @@ -20,7 +21,7 @@ */ class FactoryCommandLoader implements CommandLoaderInterface { - private $factories; + private array $factories; /** * @param callable[] $factories Indexed by command names @@ -30,18 +31,12 @@ public function __construct(array $factories) $this->factories = $factories; } - /** - * {@inheritdoc} - */ - public function has(string $name) + public function has(string $name): bool { return isset($this->factories[$name]); } - /** - * {@inheritdoc} - */ - public function get(string $name) + public function get(string $name): Command { if (!isset($this->factories[$name])) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); @@ -52,10 +47,7 @@ public function get(string $name) return $factory(); } - /** - * {@inheritdoc} - */ - public function getNames() + public function getNames(): array { return array_keys($this->factories); } diff --git a/symfony/console/Completion/CompletionInput.php b/symfony/console/Completion/CompletionInput.php index 368b94507..79c2f659a 100644 --- a/symfony/console/Completion/CompletionInput.php +++ b/symfony/console/Completion/CompletionInput.php @@ -31,11 +31,11 @@ final class CompletionInput extends ArgvInput public const TYPE_OPTION_NAME = 'option_name'; public const TYPE_NONE = 'none'; - private $tokens; - private $currentIndex; - private $completionType; - private $completionName = null; - private $completionValue = ''; + private array $tokens; + private int $currentIndex; + private string $completionType; + private ?string $completionName = null; + private string $completionValue = ''; /** * Converts a terminal string into tokens. @@ -53,7 +53,7 @@ public static function fromString(string $inputStr, int $currentIndex): self * Create an input based on an COMP_WORDS token list. * * @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv) - * @param $currentIndex the index of the cursor (e.g. COMP_CWORD) + * @param int $currentIndex the index of the cursor (e.g. COMP_CWORD) */ public static function fromTokens(array $tokens, int $currentIndex): self { @@ -64,9 +64,6 @@ public static function fromTokens(array $tokens, int $currentIndex): self return $input; } - /** - * {@inheritdoc} - */ public function bind(InputDefinition $definition): void { parent::bind($definition); @@ -84,7 +81,7 @@ public function bind(InputDefinition $definition): void return; } - if (null !== $option && $option->acceptValue()) { + if ($option?->acceptValue()) { $this->completionType = self::TYPE_OPTION_VALUE; $this->completionName = $option->getName(); $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : ''); @@ -97,7 +94,7 @@ public function bind(InputDefinition $definition): void if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) { // check if previous option accepted a value $previousOption = $this->getOptionFromToken($previousToken); - if (null !== $previousOption && $previousOption->acceptValue()) { + if ($previousOption?->acceptValue()) { $this->completionType = self::TYPE_OPTION_VALUE; $this->completionName = $previousOption->getName(); $this->completionValue = $relevantToken; @@ -144,7 +141,9 @@ public function bind(InputDefinition $definition): void * TYPE_OPTION_NAME when completing the name of an input option * TYPE_NONE when nothing should be completed * - * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component + * TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component. + * + * @return self::TYPE_* */ public function getCompletionType(): string { @@ -183,7 +182,7 @@ protected function parseToken(string $token, bool $parseOptions): bool { try { return parent::parseToken($token, $parseOptions); - } catch (RuntimeException $e) { + } catch (RuntimeException) { // suppress errors, completed input is almost never valid } diff --git a/symfony/console/Completion/CompletionSuggestions.php b/symfony/console/Completion/CompletionSuggestions.php index d8905e5ee..549bbafbd 100644 --- a/symfony/console/Completion/CompletionSuggestions.php +++ b/symfony/console/Completion/CompletionSuggestions.php @@ -20,17 +20,15 @@ */ final class CompletionSuggestions { - private $valueSuggestions = []; - private $optionSuggestions = []; + private array $valueSuggestions = []; + private array $optionSuggestions = []; /** * Add a suggested value for an input option or argument. * - * @param string|Suggestion $value - * * @return $this */ - public function suggestValue($value): self + public function suggestValue(string|Suggestion $value): static { $this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value; @@ -44,7 +42,7 @@ public function suggestValue($value): self * * @return $this */ - public function suggestValues(array $values): self + public function suggestValues(array $values): static { foreach ($values as $value) { $this->suggestValue($value); @@ -58,7 +56,7 @@ public function suggestValues(array $values): self * * @return $this */ - public function suggestOption(InputOption $option): self + public function suggestOption(InputOption $option): static { $this->optionSuggestions[] = $option; @@ -72,7 +70,7 @@ public function suggestOption(InputOption $option): self * * @return $this */ - public function suggestOptions(array $options): self + public function suggestOptions(array $options): static { foreach ($options as $option) { $this->suggestOption($option); diff --git a/symfony/console/Completion/Output/FishCompletionOutput.php b/symfony/console/Completion/Output/FishCompletionOutput.php new file mode 100644 index 000000000..d2c414e48 --- /dev/null +++ b/symfony/console/Completion/Output/FishCompletionOutput.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Guillaume Aveline + */ +class FishCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $values = $suggestions->getValueSuggestions(); + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName(); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName(); + } + } + $output->write(implode("\n", $values)); + } +} diff --git a/symfony/console/Completion/Output/ZshCompletionOutput.php b/symfony/console/Completion/Output/ZshCompletionOutput.php new file mode 100644 index 000000000..bb4ce70b5 --- /dev/null +++ b/symfony/console/Completion/Output/ZshCompletionOutput.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jitendra A + */ +class ZshCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $values = []; + foreach ($suggestions->getValueSuggestions() as $value) { + $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : ''); + } + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : ''); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : ''); + } + } + $output->write(implode("\n", $values)."\n"); + } +} diff --git a/symfony/console/Completion/Suggestion.php b/symfony/console/Completion/Suggestion.php index 6c7bc4dc4..7392965a2 100644 --- a/symfony/console/Completion/Suggestion.php +++ b/symfony/console/Completion/Suggestion.php @@ -16,13 +16,12 @@ * * @author Wouter de Jong */ -class Suggestion +class Suggestion implements \Stringable { - private $value; - - public function __construct(string $value) - { - $this->value = $value; + public function __construct( + private readonly string $value, + private readonly string $description = '' + ) { } public function getValue(): string @@ -30,6 +29,11 @@ public function getValue(): string return $this->value; } + public function getDescription(): string + { + return $this->description; + } + public function __toString(): string { return $this->getValue(); diff --git a/symfony/console/Cursor.php b/symfony/console/Cursor.php index 0c4dafb6c..69fd3821c 100644 --- a/symfony/console/Cursor.php +++ b/symfony/console/Cursor.php @@ -18,7 +18,8 @@ */ final class Cursor { - private $output; + private OutputInterface $output; + /** @var resource */ private $input; /** @@ -33,7 +34,7 @@ public function __construct(OutputInterface $output, $input = null) /** * @return $this */ - public function moveUp(int $lines = 1): self + public function moveUp(int $lines = 1): static { $this->output->write(sprintf("\x1b[%dA", $lines)); @@ -43,7 +44,7 @@ public function moveUp(int $lines = 1): self /** * @return $this */ - public function moveDown(int $lines = 1): self + public function moveDown(int $lines = 1): static { $this->output->write(sprintf("\x1b[%dB", $lines)); @@ -53,7 +54,7 @@ public function moveDown(int $lines = 1): self /** * @return $this */ - public function moveRight(int $columns = 1): self + public function moveRight(int $columns = 1): static { $this->output->write(sprintf("\x1b[%dC", $columns)); @@ -63,7 +64,7 @@ public function moveRight(int $columns = 1): self /** * @return $this */ - public function moveLeft(int $columns = 1): self + public function moveLeft(int $columns = 1): static { $this->output->write(sprintf("\x1b[%dD", $columns)); @@ -73,7 +74,7 @@ public function moveLeft(int $columns = 1): self /** * @return $this */ - public function moveToColumn(int $column): self + public function moveToColumn(int $column): static { $this->output->write(sprintf("\x1b[%dG", $column)); @@ -83,7 +84,7 @@ public function moveToColumn(int $column): self /** * @return $this */ - public function moveToPosition(int $column, int $row): self + public function moveToPosition(int $column, int $row): static { $this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column)); @@ -93,7 +94,7 @@ public function moveToPosition(int $column, int $row): self /** * @return $this */ - public function savePosition(): self + public function savePosition(): static { $this->output->write("\x1b7"); @@ -103,7 +104,7 @@ public function savePosition(): self /** * @return $this */ - public function restorePosition(): self + public function restorePosition(): static { $this->output->write("\x1b8"); @@ -113,7 +114,7 @@ public function restorePosition(): self /** * @return $this */ - public function hide(): self + public function hide(): static { $this->output->write("\x1b[?25l"); @@ -123,7 +124,7 @@ public function hide(): self /** * @return $this */ - public function show(): self + public function show(): static { $this->output->write("\x1b[?25h\x1b[?0c"); @@ -135,7 +136,7 @@ public function show(): self * * @return $this */ - public function clearLine(): self + public function clearLine(): static { $this->output->write("\x1b[2K"); @@ -157,7 +158,7 @@ public function clearLineAfter(): self * * @return $this */ - public function clearOutput(): self + public function clearOutput(): static { $this->output->write("\x1b[0J"); @@ -169,7 +170,7 @@ public function clearOutput(): self * * @return $this */ - public function clearScreen(): self + public function clearScreen(): static { $this->output->write("\x1b[2J"); @@ -183,11 +184,7 @@ public function getCurrentPosition(): array { static $isTtySupported; - if (null === $isTtySupported && \function_exists('proc_open')) { - $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); - } - - if (!$isTtySupported) { + if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) { return [1, 1]; } diff --git a/symfony/console/DataCollector/CommandDataCollector.php b/symfony/console/DataCollector/CommandDataCollector.php new file mode 100644 index 000000000..45138c7dc --- /dev/null +++ b/symfony/console/DataCollector/CommandDataCollector.php @@ -0,0 +1,234 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\DataCollector; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Debug\CliRequest; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalMap; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * @internal + * + * @author Jules Pietri + */ +final class CommandDataCollector extends DataCollector +{ + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + if (!$request instanceof CliRequest) { + return; + } + + $command = $request->command; + $application = $command->getApplication(); + + $this->data = [ + 'command' => $this->cloneVar($command->command), + 'exit_code' => $command->exitCode, + 'interrupted_by_signal' => $command->interruptedBySignal, + 'duration' => $command->duration, + 'max_memory_usage' => $command->maxMemoryUsage, + 'verbosity_level' => match ($command->output->getVerbosity()) { + OutputInterface::VERBOSITY_QUIET => 'quiet', + OutputInterface::VERBOSITY_NORMAL => 'normal', + OutputInterface::VERBOSITY_VERBOSE => 'verbose', + OutputInterface::VERBOSITY_VERY_VERBOSE => 'very verbose', + OutputInterface::VERBOSITY_DEBUG => 'debug', + }, + 'interactive' => $command->isInteractive, + 'validate_input' => !$command->ignoreValidation, + 'enabled' => $command->isEnabled(), + 'visible' => !$command->isHidden(), + 'input' => $this->cloneVar($command->input), + 'output' => $this->cloneVar($command->output), + 'interactive_inputs' => array_map($this->cloneVar(...), $command->interactiveInputs), + 'signalable' => $command->getSubscribedSignals(), + 'handled_signals' => $command->handledSignals, + 'helper_set' => array_map($this->cloneVar(...), iterator_to_array($command->getHelperSet())), + ]; + + $baseDefinition = $application->getDefinition(); + + foreach ($command->arguments as $argName => $argValue) { + if ($baseDefinition->hasArgument($argName)) { + $this->data['application_inputs'][$argName] = $this->cloneVar($argValue); + } else { + $this->data['arguments'][$argName] = $this->cloneVar($argValue); + } + } + + foreach ($command->options as $optName => $optValue) { + if ($baseDefinition->hasOption($optName)) { + $this->data['application_inputs']['--'.$optName] = $this->cloneVar($optValue); + } else { + $this->data['options'][$optName] = $this->cloneVar($optValue); + } + } + } + + public function getName(): string + { + return 'command'; + } + + /** + * @return array{ + * class?: class-string, + * executor?: string, + * file: string, + * line: int, + * } + */ + public function getCommand(): array + { + $class = $this->data['command']->getType(); + $r = new \ReflectionMethod($class, 'execute'); + + if (Command::class !== $r->getDeclaringClass()) { + return [ + 'executor' => $class.'::'.$r->name, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ]; + } + + $r = new \ReflectionClass($class); + + return [ + 'class' => $class, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ]; + } + + public function getInterruptedBySignal(): ?string + { + if (isset($this->data['interrupted_by_signal'])) { + return sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']); + } + + return null; + } + + public function getDuration(): string + { + return $this->data['duration']; + } + + public function getMaxMemoryUsage(): string + { + return $this->data['max_memory_usage']; + } + + public function getVerbosityLevel(): string + { + return $this->data['verbosity_level']; + } + + public function getInteractive(): bool + { + return $this->data['interactive']; + } + + public function getValidateInput(): bool + { + return $this->data['validate_input']; + } + + public function getEnabled(): bool + { + return $this->data['enabled']; + } + + public function getVisible(): bool + { + return $this->data['visible']; + } + + public function getInput(): Data + { + return $this->data['input']; + } + + public function getOutput(): Data + { + return $this->data['output']; + } + + /** + * @return Data[] + */ + public function getArguments(): array + { + return $this->data['arguments'] ?? []; + } + + /** + * @return Data[] + */ + public function getOptions(): array + { + return $this->data['options'] ?? []; + } + + /** + * @return Data[] + */ + public function getApplicationInputs(): array + { + return $this->data['application_inputs'] ?? []; + } + + /** + * @return Data[] + */ + public function getInteractiveInputs(): array + { + return $this->data['interactive_inputs'] ?? []; + } + + public function getSignalable(): array + { + return array_map( + static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal), + $this->data['signalable'] + ); + } + + public function getHandledSignals(): array + { + $keys = array_map( + static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal), + array_keys($this->data['handled_signals']) + ); + + return array_combine($keys, array_values($this->data['handled_signals'])); + } + + /** + * @return Data[] + */ + public function getHelperSet(): array + { + return $this->data['helper_set'] ?? []; + } + + public function reset(): void + { + $this->data = []; + } +} diff --git a/symfony/console/Debug/CliRequest.php b/symfony/console/Debug/CliRequest.php new file mode 100644 index 000000000..b023db07a --- /dev/null +++ b/symfony/console/Debug/CliRequest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Debug; + +use Symfony\Component\Console\Command\TraceableCommand; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @internal + */ +final class CliRequest extends Request +{ + public function __construct( + public readonly TraceableCommand $command, + ) { + parent::__construct( + attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'], + server: $_SERVER, + ); + } + + // Methods below allow to populate a profile, thus enable search and filtering + public function getUri(): string + { + if ($this->server->has('SYMFONY_CLI_BINARY_NAME')) { + $binary = $this->server->get('SYMFONY_CLI_BINARY_NAME').' console'; + } else { + $binary = $this->server->get('argv')[0]; + } + + return $binary.' '.$this->command->input; + } + + public function getMethod(): string + { + return $this->command->isInteractive ? 'INTERACTIVE' : 'BATCH'; + } + + public function getResponse(): Response + { + return new class($this->command->exitCode) extends Response { + public function __construct(private readonly int $exitCode) + { + parent::__construct(); + } + + public function getStatusCode(): int + { + return $this->exitCode; + } + }; + } + + public function getClientIp(): string + { + $application = $this->command->getApplication(); + + return $application->getName().' '.$application->getVersion(); + } +} diff --git a/symfony/console/DependencyInjection/AddConsoleCommandPass.php b/symfony/console/DependencyInjection/AddConsoleCommandPass.php index 1fbb212e7..27705ddb6 100644 --- a/symfony/console/DependencyInjection/AddConsoleCommandPass.php +++ b/symfony/console/DependencyInjection/AddConsoleCommandPass.php @@ -29,33 +29,19 @@ */ class AddConsoleCommandPass implements CompilerPassInterface { - private $commandLoaderServiceId; - private $commandTag; - private $noPreloadTag; - private $privateTagName; - - public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private') - { - if (0 < \func_num_args()) { - trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); - } - - $this->commandLoaderServiceId = $commandLoaderServiceId; - $this->commandTag = $commandTag; - $this->noPreloadTag = $noPreloadTag; - $this->privateTagName = $privateTagName; - } - + /** + * @return void + */ public function process(ContainerBuilder $container) { - $commandServices = $container->findTaggedServiceIds($this->commandTag, true); + $commandServices = $container->findTaggedServiceIds('console.command', true); $lazyCommandMap = []; $lazyCommandRefs = []; $serviceIds = []; foreach ($commandServices as $id => $tags) { $definition = $container->getDefinition($id); - $definition->addTag($this->noPreloadTag); + $definition->addTag('container.no_preload'); $class = $container->getParameterBag()->resolveValue($definition->getClass()); if (isset($tags[0]['command'])) { @@ -65,7 +51,7 @@ public function process(ContainerBuilder $container) throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(Command::class)) { - throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class)); } $aliases = str_replace('%', '%%', $class::getDefaultName() ?? ''); } @@ -78,7 +64,7 @@ public function process(ContainerBuilder $container) } if (null === $commandName) { - if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag($this->privateTagName)) { + if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) { $commandId = 'console.command.public_alias.'.$id; $container->setAlias($commandId, $id)->setPublic(true); $id = $commandId; @@ -104,7 +90,7 @@ public function process(ContainerBuilder $container) $lazyCommandMap[$tag['command']] = $id; } - $description = $description ?? $tag['description'] ?? null; + $description ??= $tag['description'] ?? null; } $definition->addMethodCall('setName', [$commandName]); @@ -122,7 +108,7 @@ public function process(ContainerBuilder $container) throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(Command::class)) { - throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class)); } $description = str_replace('%', '%%', $class::getDefaultDescription() ?? ''); } @@ -138,9 +124,9 @@ public function process(ContainerBuilder $container) } $container - ->register($this->commandLoaderServiceId, ContainerCommandLoader::class) + ->register('console.command_loader', ContainerCommandLoader::class) ->setPublic(true) - ->addTag($this->noPreloadTag) + ->addTag('container.no_preload') ->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]); $container->setParameter('console.command.ids', $serviceIds); diff --git a/symfony/console/Descriptor/ApplicationDescription.php b/symfony/console/Descriptor/ApplicationDescription.php index eb11b4f91..ef9e8a63b 100644 --- a/symfony/console/Descriptor/ApplicationDescription.php +++ b/symfony/console/Descriptor/ApplicationDescription.php @@ -24,24 +24,20 @@ class ApplicationDescription { public const GLOBAL_NAMESPACE = '_global'; - private $application; - private $namespace; - private $showHidden; - - /** - * @var array - */ - private $namespaces; + private Application $application; + private ?string $namespace; + private bool $showHidden; + private array $namespaces; /** * @var array */ - private $commands; + private array $commands; /** * @var array */ - private $aliases; + private array $aliases = []; public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false) { @@ -52,7 +48,7 @@ public function __construct(Application $application, ?string $namespace = null, public function getNamespaces(): array { - if (null === $this->namespaces) { + if (!isset($this->namespaces)) { $this->inspectApplication(); } @@ -64,7 +60,7 @@ public function getNamespaces(): array */ public function getCommands(): array { - if (null === $this->commands) { + if (!isset($this->commands)) { $this->inspectApplication(); } @@ -83,7 +79,7 @@ public function getCommand(string $name): Command return $this->commands[$name] ?? $this->aliases[$name]; } - private function inspectApplication() + private function inspectApplication(): void { $this->commands = []; $this->namespaces = []; diff --git a/symfony/console/Descriptor/Descriptor.php b/symfony/console/Descriptor/Descriptor.php index a3648301f..7b2509c60 100644 --- a/symfony/console/Descriptor/Descriptor.php +++ b/symfony/console/Descriptor/Descriptor.php @@ -26,43 +26,23 @@ */ abstract class Descriptor implements DescriptorInterface { - /** - * @var OutputInterface - */ - protected $output; + protected OutputInterface $output; - /** - * {@inheritdoc} - */ - public function describe(OutputInterface $output, object $object, array $options = []) + public function describe(OutputInterface $output, object $object, array $options = []): void { $this->output = $output; - switch (true) { - case $object instanceof InputArgument: - $this->describeInputArgument($object, $options); - break; - case $object instanceof InputOption: - $this->describeInputOption($object, $options); - break; - case $object instanceof InputDefinition: - $this->describeInputDefinition($object, $options); - break; - case $object instanceof Command: - $this->describeCommand($object, $options); - break; - case $object instanceof Application: - $this->describeApplication($object, $options); - break; - default: - throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))); - } + match (true) { + $object instanceof InputArgument => $this->describeInputArgument($object, $options), + $object instanceof InputOption => $this->describeInputOption($object, $options), + $object instanceof InputDefinition => $this->describeInputDefinition($object, $options), + $object instanceof Command => $this->describeCommand($object, $options), + $object instanceof Application => $this->describeApplication($object, $options), + default => throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))), + }; } - /** - * Writes content to output. - */ - protected function write(string $content, bool $decorated = false) + protected function write(string $content, bool $decorated = false): void { $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); } @@ -70,25 +50,25 @@ protected function write(string $content, bool $decorated = false) /** * Describes an InputArgument instance. */ - abstract protected function describeInputArgument(InputArgument $argument, array $options = []); + abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void; /** * Describes an InputOption instance. */ - abstract protected function describeInputOption(InputOption $option, array $options = []); + abstract protected function describeInputOption(InputOption $option, array $options = []): void; /** * Describes an InputDefinition instance. */ - abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []); + abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void; /** * Describes a Command instance. */ - abstract protected function describeCommand(Command $command, array $options = []); + abstract protected function describeCommand(Command $command, array $options = []): void; /** * Describes an Application instance. */ - abstract protected function describeApplication(Application $application, array $options = []); + abstract protected function describeApplication(Application $application, array $options = []): void; } diff --git a/symfony/console/Descriptor/DescriptorInterface.php b/symfony/console/Descriptor/DescriptorInterface.php index ebea30367..ab468a256 100644 --- a/symfony/console/Descriptor/DescriptorInterface.php +++ b/symfony/console/Descriptor/DescriptorInterface.php @@ -20,5 +20,8 @@ */ interface DescriptorInterface { + /** + * @return void + */ public function describe(OutputInterface $output, object $object, array $options = []); } diff --git a/symfony/console/Descriptor/JsonDescriptor.php b/symfony/console/Descriptor/JsonDescriptor.php index 1d2865941..956303709 100644 --- a/symfony/console/Descriptor/JsonDescriptor.php +++ b/symfony/console/Descriptor/JsonDescriptor.php @@ -26,18 +26,12 @@ */ class JsonDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ - protected function describeInputArgument(InputArgument $argument, array $options = []) + protected function describeInputArgument(InputArgument $argument, array $options = []): void { $this->writeData($this->getInputArgumentData($argument), $options); } - /** - * {@inheritdoc} - */ - protected function describeInputOption(InputOption $option, array $options = []) + protected function describeInputOption(InputOption $option, array $options = []): void { $this->writeData($this->getInputOptionData($option), $options); if ($option->isNegatable()) { @@ -45,26 +39,17 @@ protected function describeInputOption(InputOption $option, array $options = []) } } - /** - * {@inheritdoc} - */ - protected function describeInputDefinition(InputDefinition $definition, array $options = []) + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void { $this->writeData($this->getInputDefinitionData($definition), $options); } - /** - * {@inheritdoc} - */ - protected function describeCommand(Command $command, array $options = []) + protected function describeCommand(Command $command, array $options = []): void { $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options); } - /** - * {@inheritdoc} - */ - protected function describeApplication(Application $application, array $options = []) + protected function describeApplication(Application $application, array $options = []): void { $describedNamespace = $options['namespace'] ?? null; $description = new ApplicationDescription($application, $describedNamespace, true); @@ -96,7 +81,7 @@ protected function describeApplication(Application $application, array $options /** * Writes data as json. */ - private function writeData(array $data, array $options) + private function writeData(array $data, array $options): void { $flags = $options['json_encoding'] ?? 0; diff --git a/symfony/console/Descriptor/MarkdownDescriptor.php b/symfony/console/Descriptor/MarkdownDescriptor.php index 21ceca6c2..b3f16ee90 100644 --- a/symfony/console/Descriptor/MarkdownDescriptor.php +++ b/symfony/console/Descriptor/MarkdownDescriptor.php @@ -28,10 +28,7 @@ */ class MarkdownDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ - public function describe(OutputInterface $output, object $object, array $options = []) + public function describe(OutputInterface $output, object $object, array $options = []): void { $decorated = $output->isDecorated(); $output->setDecorated(false); @@ -41,18 +38,12 @@ public function describe(OutputInterface $output, object $object, array $options $output->setDecorated($decorated); } - /** - * {@inheritdoc} - */ - protected function write(string $content, bool $decorated = true) + protected function write(string $content, bool $decorated = true): void { parent::write($content, $decorated); } - /** - * {@inheritdoc} - */ - protected function describeInputArgument(InputArgument $argument, array $options = []) + protected function describeInputArgument(InputArgument $argument, array $options = []): void { $this->write( '#### `'.($argument->getName() ?: '')."`\n\n" @@ -63,10 +54,7 @@ protected function describeInputArgument(InputArgument $argument, array $options ); } - /** - * {@inheritdoc} - */ - protected function describeInputOption(InputOption $option, array $options = []) + protected function describeInputOption(InputOption $option, array $options = []): void { $name = '--'.$option->getName(); if ($option->isNegatable()) { @@ -87,18 +75,13 @@ protected function describeInputOption(InputOption $option, array $options = []) ); } - /** - * {@inheritdoc} - */ - protected function describeInputDefinition(InputDefinition $definition, array $options = []) + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void { if ($showArguments = \count($definition->getArguments()) > 0) { $this->write('### Arguments'); foreach ($definition->getArguments() as $argument) { $this->write("\n\n"); - if (null !== $describeInputArgument = $this->describeInputArgument($argument)) { - $this->write($describeInputArgument); - } + $this->describeInputArgument($argument); } } @@ -110,17 +93,12 @@ protected function describeInputDefinition(InputDefinition $definition, array $o $this->write('### Options'); foreach ($definition->getOptions() as $option) { $this->write("\n\n"); - if (null !== $describeInputOption = $this->describeInputOption($option)) { - $this->write($describeInputOption); - } + $this->describeInputOption($option); } } } - /** - * {@inheritdoc} - */ - protected function describeCommand(Command $command, array $options = []) + protected function describeCommand(Command $command, array $options = []): void { if ($options['short'] ?? false) { $this->write( @@ -128,9 +106,7 @@ protected function describeCommand(Command $command, array $options = []) .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" .($command->getDescription() ? $command->getDescription()."\n\n" : '') .'### Usage'."\n\n" - .array_reduce($command->getAliases(), function ($carry, $usage) { - return $carry.'* `'.$usage.'`'."\n"; - }) + .array_reduce($command->getAliases(), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n") ); return; @@ -143,9 +119,7 @@ protected function describeCommand(Command $command, array $options = []) .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" .($command->getDescription() ? $command->getDescription()."\n\n" : '') .'### Usage'."\n\n" - .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) { - return $carry.'* `'.$usage.'`'."\n"; - }) + .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n") ); if ($help = $command->getProcessedHelp()) { @@ -160,10 +134,7 @@ protected function describeCommand(Command $command, array $options = []) } } - /** - * {@inheritdoc} - */ - protected function describeApplication(Application $application, array $options = []) + protected function describeApplication(Application $application, array $options = []): void { $describedNamespace = $options['namespace'] ?? null; $description = new ApplicationDescription($application, $describedNamespace); @@ -178,16 +149,12 @@ protected function describeApplication(Application $application, array $options } $this->write("\n\n"); - $this->write(implode("\n", array_map(function ($commandName) use ($description) { - return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())); - }, $namespace['commands']))); + $this->write(implode("\n", array_map(fn ($commandName) => sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands']))); } foreach ($description->getCommands() as $command) { $this->write("\n\n"); - if (null !== $describeCommand = $this->describeCommand($command, $options)) { - $this->write($describeCommand); - } + $this->describeCommand($command, $options); } } diff --git a/symfony/console/Descriptor/ReStructuredTextDescriptor.php b/symfony/console/Descriptor/ReStructuredTextDescriptor.php new file mode 100644 index 000000000..d4423fd34 --- /dev/null +++ b/symfony/console/Descriptor/ReStructuredTextDescriptor.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\String\UnicodeString; + +class ReStructuredTextDescriptor extends Descriptor +{ + //

+ private string $partChar = '='; + //

+ private string $chapterChar = '-'; + //

+ private string $sectionChar = '~'; + //

+ private string $subsectionChar = '.'; + //

+ private string $subsubsectionChar = '^'; + //
+ private string $paragraphsChar = '"'; + + private array $visibleNamespaces = []; + + public function describe(OutputInterface $output, object $object, array $options = []): void + { + $decorated = $output->isDecorated(); + $output->setDecorated(false); + + parent::describe($output, $object, $options); + + $output->setDecorated($decorated); + } + + /** + * Override parent method to set $decorated = true. + */ + protected function write(string $content, bool $decorated = true): void + { + parent::write($content, $decorated); + } + + protected function describeInputArgument(InputArgument $argument, array $options = []): void + { + $this->write( + $argument->getName() ?: ''."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n" + .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') + .'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n" + .'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n" + .'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``' + ); + } + + protected function describeInputOption(InputOption $option, array $options = []): void + { + $name = '\-\-'.$option->getName(); + if ($option->isNegatable()) { + $name .= '|\-\-no-'.$option->getName(); + } + if ($option->getShortcut()) { + $name .= '|-'.str_replace('|', '|-', $option->getShortcut()); + } + + $optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : ''; + $optionDescription = (new UnicodeString($optionDescription))->ascii(); + $this->write( + $name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n" + .$optionDescription + .'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n" + .'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n" + .'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n" + .'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n" + .'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n" + ); + } + + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void + { + if ($showArguments = ((bool) $definition->getArguments())) { + $this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9))."\n\n"; + foreach ($definition->getArguments() as $argument) { + $this->write("\n\n"); + $this->describeInputArgument($argument); + } + } + + if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) { + if ($showArguments) { + $this->write("\n\n"); + } + + $this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n"); + foreach ($nonDefaultOptions as $option) { + $this->describeInputOption($option); + $this->write("\n"); + } + } + } + + protected function describeCommand(Command $command, array $options = []): void + { + if ($options['short'] ?? false) { + $this->write( + '``'.$command->getName()."``\n" + .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + ."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n" + .array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n") + ); + + return; + } + + $command->mergeApplicationDefinition(false); + + foreach ($command->getAliases() as $alias) { + $this->write('.. _'.$alias.":\n\n"); + } + $this->write( + $command->getName()."\n" + .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + ."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n" + .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n") + ); + + if ($help = $command->getProcessedHelp()) { + $this->write("\n"); + $this->write($help); + } + + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->write("\n\n"); + $this->describeInputDefinition($definition); + } + } + + protected function describeApplication(Application $application, array $options = []): void + { + $description = new ApplicationDescription($application, $options['namespace'] ?? null); + $title = $this->getApplicationTitle($application); + + $this->write($title."\n".str_repeat($this->partChar, Helper::width($title))); + $this->createTableOfContents($description, $application); + $this->describeCommands($application, $options); + } + + private function getApplicationTitle(Application $application): string + { + if ('UNKNOWN' === $application->getName()) { + return 'Console Tool'; + } + if ('UNKNOWN' !== $application->getVersion()) { + return sprintf('%s %s', $application->getName(), $application->getVersion()); + } + + return $application->getName(); + } + + private function describeCommands($application, array $options): void + { + $title = 'Commands'; + $this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n"); + foreach ($this->visibleNamespaces as $namespace) { + if ('_global' === $namespace) { + $commands = $application->all(''); + $this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n"); + } else { + $commands = $application->all($namespace); + $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n"); + } + + foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) { + $this->describeCommand($command, $options); + $this->write("\n\n"); + } + } + } + + private function createTableOfContents(ApplicationDescription $description, Application $application): void + { + $this->setVisibleNamespaces($description); + $chapterTitle = 'Table of Contents'; + $this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n"); + foreach ($this->visibleNamespaces as $namespace) { + if ('_global' === $namespace) { + $commands = $application->all(''); + } else { + $commands = $application->all($namespace); + $this->write("\n\n"); + $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n"); + } + $commands = $this->removeAliasesAndHiddenCommands($commands); + + $this->write("\n\n"); + $this->write(implode("\n", array_map(static fn ($commandName) => sprintf('- `%s`_', $commandName), array_keys($commands)))); + } + } + + private function getNonDefaultOptions(InputDefinition $definition): array + { + $globalOptions = [ + 'help', + 'quiet', + 'verbose', + 'version', + 'ansi', + 'no-interaction', + ]; + $nonDefaultOptions = []; + foreach ($definition->getOptions() as $option) { + // Skip global options. + if (!\in_array($option->getName(), $globalOptions)) { + $nonDefaultOptions[] = $option; + } + } + + return $nonDefaultOptions; + } + + private function setVisibleNamespaces(ApplicationDescription $description): void + { + $commands = $description->getCommands(); + foreach ($description->getNamespaces() as $namespace) { + try { + $namespaceCommands = $namespace['commands']; + foreach ($namespaceCommands as $key => $commandName) { + if (!\array_key_exists($commandName, $commands)) { + // If the array key does not exist, then this is an alias. + unset($namespaceCommands[$key]); + } elseif ($commands[$commandName]->isHidden()) { + unset($namespaceCommands[$key]); + } + } + if (!$namespaceCommands) { + // If the namespace contained only aliases or hidden commands, skip the namespace. + continue; + } + } catch (\Exception) { + } + $this->visibleNamespaces[] = $namespace['id']; + } + } + + private function removeAliasesAndHiddenCommands(array $commands): array + { + foreach ($commands as $key => $command) { + if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) { + unset($commands[$key]); + } + } + unset($commands['completion']); + + return $commands; + } +} diff --git a/symfony/console/Descriptor/TextDescriptor.php b/symfony/console/Descriptor/TextDescriptor.php index fbb140ae7..d04d10238 100644 --- a/symfony/console/Descriptor/TextDescriptor.php +++ b/symfony/console/Descriptor/TextDescriptor.php @@ -28,10 +28,7 @@ */ class TextDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ - protected function describeInputArgument(InputArgument $argument, array $options = []) + protected function describeInputArgument(InputArgument $argument, array $options = []): void { if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); @@ -51,10 +48,7 @@ protected function describeInputArgument(InputArgument $argument, array $options ), $options); } - /** - * {@inheritdoc} - */ - protected function describeInputOption(InputOption $option, array $options = []) + protected function describeInputOption(InputOption $option, array $options = []): void { if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); @@ -89,10 +83,7 @@ protected function describeInputOption(InputOption $option, array $options = []) ), $options); } - /** - * {@inheritdoc} - */ - protected function describeInputDefinition(InputDefinition $definition, array $options = []) + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void { $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); foreach ($definition->getArguments() as $argument) { @@ -131,10 +122,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o } } - /** - * {@inheritdoc} - */ - protected function describeCommand(Command $command, array $options = []) + protected function describeCommand(Command $command, array $options = []): void { $command->mergeApplicationDefinition(false); @@ -169,10 +157,7 @@ protected function describeCommand(Command $command, array $options = []) } } - /** - * {@inheritdoc} - */ - protected function describeApplication(Application $application, array $options = []) + protected function describeApplication(Application $application, array $options = []): void { $describedNamespace = $options['namespace'] ?? null; $description = new ApplicationDescription($application, $describedNamespace); @@ -208,9 +193,7 @@ protected function describeApplication(Application $application, array $options } // calculate max. width based on available commands per namespace - $width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) { - return array_intersect($namespace['commands'], array_keys($commands)); - }, array_values($namespaces))))); + $width = $this->getColumnWidth(array_merge(...array_values(array_map(fn ($namespace) => array_intersect($namespace['commands'], array_keys($commands)), array_values($namespaces))))); if ($describedNamespace) { $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); @@ -219,9 +202,7 @@ protected function describeApplication(Application $application, array $options } foreach ($namespaces as $namespace) { - $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) { - return isset($commands[$name]); - }); + $namespace['commands'] = array_filter($namespace['commands'], fn ($name) => isset($commands[$name])); if (!$namespace['commands']) { continue; @@ -245,10 +226,7 @@ protected function describeApplication(Application $application, array $options } } - /** - * {@inheritdoc} - */ - private function writeText(string $content, array $options = []) + private function writeText(string $content, array $options = []): void { $this->write( isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, @@ -273,10 +251,8 @@ private function getCommandAliasesText(Command $command): string /** * Formats input option/argument default value. - * - * @param mixed $default */ - private function formatDefaultValue($default): string + private function formatDefaultValue(mixed $default): string { if (\INF === $default) { return 'INF'; diff --git a/symfony/console/Descriptor/XmlDescriptor.php b/symfony/console/Descriptor/XmlDescriptor.php index f17e5f1f2..866c71856 100644 --- a/symfony/console/Descriptor/XmlDescriptor.php +++ b/symfony/console/Descriptor/XmlDescriptor.php @@ -120,42 +120,27 @@ public function getApplicationDocument(Application $application, ?string $namesp return $dom; } - /** - * {@inheritdoc} - */ - protected function describeInputArgument(InputArgument $argument, array $options = []) + protected function describeInputArgument(InputArgument $argument, array $options = []): void { $this->writeDocument($this->getInputArgumentDocument($argument)); } - /** - * {@inheritdoc} - */ - protected function describeInputOption(InputOption $option, array $options = []) + protected function describeInputOption(InputOption $option, array $options = []): void { $this->writeDocument($this->getInputOptionDocument($option)); } - /** - * {@inheritdoc} - */ - protected function describeInputDefinition(InputDefinition $definition, array $options = []) + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void { $this->writeDocument($this->getInputDefinitionDocument($definition)); } - /** - * {@inheritdoc} - */ - protected function describeCommand(Command $command, array $options = []) + protected function describeCommand(Command $command, array $options = []): void { $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false)); } - /** - * {@inheritdoc} - */ - protected function describeApplication(Application $application, array $options = []) + protected function describeApplication(Application $application, array $options = []): void { $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false)); } @@ -163,7 +148,7 @@ protected function describeApplication(Application $application, array $options /** * Appends document children to parent node. */ - private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) + private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void { foreach ($importedParent->childNodes as $childNode) { $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true)); @@ -173,7 +158,7 @@ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) /** * Writes DOM document. */ - private function writeDocument(\DOMDocument $dom) + private function writeDocument(\DOMDocument $dom): void { $dom->formatOutput = true; $this->write($dom->saveXML()); diff --git a/symfony/console/Event/ConsoleCommandEvent.php b/symfony/console/Event/ConsoleCommandEvent.php index 1b4f9f9b1..0757a23f6 100644 --- a/symfony/console/Event/ConsoleCommandEvent.php +++ b/symfony/console/Event/ConsoleCommandEvent.php @@ -29,7 +29,7 @@ final class ConsoleCommandEvent extends ConsoleEvent /** * Indicates if the command should be run or skipped. */ - private $commandShouldRun = true; + private bool $commandShouldRun = true; /** * Disables the command, so it won't be run. diff --git a/symfony/console/Event/ConsoleErrorEvent.php b/symfony/console/Event/ConsoleErrorEvent.php index d4c26493f..7be2ff83e 100644 --- a/symfony/console/Event/ConsoleErrorEvent.php +++ b/symfony/console/Event/ConsoleErrorEvent.php @@ -22,8 +22,8 @@ */ final class ConsoleErrorEvent extends ConsoleEvent { - private $error; - private $exitCode; + private \Throwable $error; + private int $exitCode; public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null) { @@ -47,7 +47,6 @@ public function setExitCode(int $exitCode): void $this->exitCode = $exitCode; $r = new \ReflectionProperty($this->error, 'code'); - $r->setAccessible(true); $r->setValue($this->error, $this->exitCode); } diff --git a/symfony/console/Event/ConsoleEvent.php b/symfony/console/Event/ConsoleEvent.php index be7937d51..6ba1615fe 100644 --- a/symfony/console/Event/ConsoleEvent.php +++ b/symfony/console/Event/ConsoleEvent.php @@ -25,8 +25,8 @@ class ConsoleEvent extends Event { protected $command; - private $input; - private $output; + private InputInterface $input; + private OutputInterface $output; public function __construct(?Command $command, InputInterface $input, OutputInterface $output) { @@ -37,30 +37,24 @@ public function __construct(?Command $command, InputInterface $input, OutputInte /** * Gets the command that is executed. - * - * @return Command|null */ - public function getCommand() + public function getCommand(): ?Command { return $this->command; } /** * Gets the input instance. - * - * @return InputInterface */ - public function getInput() + public function getInput(): InputInterface { return $this->input; } /** * Gets the output instance. - * - * @return OutputInterface */ - public function getOutput() + public function getOutput(): OutputInterface { return $this->output; } diff --git a/symfony/console/Event/ConsoleSignalEvent.php b/symfony/console/Event/ConsoleSignalEvent.php index ef13ed2f5..95af1f915 100644 --- a/symfony/console/Event/ConsoleSignalEvent.php +++ b/symfony/console/Event/ConsoleSignalEvent.php @@ -20,16 +20,37 @@ */ final class ConsoleSignalEvent extends ConsoleEvent { - private $handlingSignal; + private int $handlingSignal; + private int|false $exitCode; - public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal) + public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal, int|false $exitCode = 0) { parent::__construct($command, $input, $output); $this->handlingSignal = $handlingSignal; + $this->exitCode = $exitCode; } public function getHandlingSignal(): int { return $this->handlingSignal; } + + public function setExitCode(int $exitCode): void + { + if ($exitCode < 0 || $exitCode > 255) { + throw new \InvalidArgumentException('Exit code must be between 0 and 255.'); + } + + $this->exitCode = $exitCode; + } + + public function abortExit(): void + { + $this->exitCode = false; + } + + public function getExitCode(): int|false + { + return $this->exitCode; + } } diff --git a/symfony/console/Event/ConsoleTerminateEvent.php b/symfony/console/Event/ConsoleTerminateEvent.php index 190038d1a..38f7253a5 100644 --- a/symfony/console/Event/ConsoleTerminateEvent.php +++ b/symfony/console/Event/ConsoleTerminateEvent.php @@ -19,16 +19,18 @@ * Allows to manipulate the exit code of a command after its execution. * * @author Francesco Levorato + * @author Jules Pietri */ final class ConsoleTerminateEvent extends ConsoleEvent { - private $exitCode; - - public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode) - { + public function __construct( + Command $command, + InputInterface $input, + OutputInterface $output, + private int $exitCode, + private readonly ?int $interruptingSignal = null, + ) { parent::__construct($command, $input, $output); - - $this->setExitCode($exitCode); } public function setExitCode(int $exitCode): void @@ -40,4 +42,9 @@ public function getExitCode(): int { return $this->exitCode; } + + public function getInterruptingSignal(): ?int + { + return $this->interruptingSignal; + } } diff --git a/symfony/console/EventListener/ErrorListener.php b/symfony/console/EventListener/ErrorListener.php index e9c9e3ea4..c9ec24434 100644 --- a/symfony/console/EventListener/ErrorListener.php +++ b/symfony/console/EventListener/ErrorListener.php @@ -24,13 +24,16 @@ */ class ErrorListener implements EventSubscriberInterface { - private $logger; + private ?LoggerInterface $logger; public function __construct(?LoggerInterface $logger = null) { $this->logger = $logger; } + /** + * @return void + */ public function onConsoleError(ConsoleErrorEvent $event) { if (null === $this->logger) { @@ -48,6 +51,9 @@ public function onConsoleError(ConsoleErrorEvent $event) $this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]); } + /** + * @return void + */ public function onConsoleTerminate(ConsoleTerminateEvent $event) { if (null === $this->logger) { @@ -69,7 +75,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event) $this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ ConsoleEvents::ERROR => ['onConsoleError', -128], @@ -79,10 +85,10 @@ public static function getSubscribedEvents() private static function getInputString(ConsoleEvent $event): ?string { - $commandName = $event->getCommand() ? $event->getCommand()->getName() : null; + $commandName = $event->getCommand()?->getName(); $input = $event->getInput(); - if (method_exists($input, '__toString')) { + if ($input instanceof \Stringable) { if ($commandName) { return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input); } diff --git a/symfony/console/Exception/CommandNotFoundException.php b/symfony/console/Exception/CommandNotFoundException.php index 81ec318ab..541b32b23 100644 --- a/symfony/console/Exception/CommandNotFoundException.php +++ b/symfony/console/Exception/CommandNotFoundException.php @@ -18,7 +18,7 @@ */ class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface { - private $alternatives; + private array $alternatives; /** * @param string $message Exception message to throw @@ -36,7 +36,7 @@ public function __construct(string $message, array $alternatives = [], int $code /** * @return string[] */ - public function getAlternatives() + public function getAlternatives(): array { return $this->alternatives; } diff --git a/symfony/console/Exception/RunCommandFailedException.php b/symfony/console/Exception/RunCommandFailedException.php new file mode 100644 index 000000000..5d87ec949 --- /dev/null +++ b/symfony/console/Exception/RunCommandFailedException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +use Symfony\Component\Console\Messenger\RunCommandContext; + +/** + * @author Kevin Bond + */ +final class RunCommandFailedException extends RuntimeException +{ + public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context) + { + parent::__construct( + $exception instanceof \Throwable ? $exception->getMessage() : $exception, + $exception instanceof \Throwable ? $exception->getCode() : 0, + $exception instanceof \Throwable ? $exception : null, + ); + } +} diff --git a/symfony/console/Formatter/NullOutputFormatter.php b/symfony/console/Formatter/NullOutputFormatter.php index d770e1465..5c11c7644 100644 --- a/symfony/console/Formatter/NullOutputFormatter.php +++ b/symfony/console/Formatter/NullOutputFormatter.php @@ -16,52 +16,34 @@ */ final class NullOutputFormatter implements OutputFormatterInterface { - private $style; + private NullOutputFormatterStyle $style; - /** - * {@inheritdoc} - */ public function format(?string $message): ?string { return null; } - /** - * {@inheritdoc} - */ public function getStyle(string $name): OutputFormatterStyleInterface { // to comply with the interface we must return a OutputFormatterStyleInterface - return $this->style ?? $this->style = new NullOutputFormatterStyle(); + return $this->style ??= new NullOutputFormatterStyle(); } - /** - * {@inheritdoc} - */ public function hasStyle(string $name): bool { return false; } - /** - * {@inheritdoc} - */ public function isDecorated(): bool { return false; } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated): void { // do nothing } - /** - * {@inheritdoc} - */ public function setStyle(string $name, OutputFormatterStyleInterface $style): void { // do nothing diff --git a/symfony/console/Formatter/NullOutputFormatterStyle.php b/symfony/console/Formatter/NullOutputFormatterStyle.php index afd3d0043..ae23decb1 100644 --- a/symfony/console/Formatter/NullOutputFormatterStyle.php +++ b/symfony/console/Formatter/NullOutputFormatterStyle.php @@ -16,49 +16,37 @@ */ final class NullOutputFormatterStyle implements OutputFormatterStyleInterface { - /** - * {@inheritdoc} - */ public function apply(string $text): string { return $text; } - /** - * {@inheritdoc} - */ public function setBackground(?string $color = null): void { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } // do nothing } - /** - * {@inheritdoc} - */ public function setForeground(?string $color = null): void { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } // do nothing } - /** - * {@inheritdoc} - */ public function setOption(string $option): void { // do nothing } - /** - * {@inheritdoc} - */ public function setOptions(array $options): void { // do nothing } - /** - * {@inheritdoc} - */ public function unsetOption(string $option): void { // do nothing diff --git a/symfony/console/Formatter/OutputFormatter.php b/symfony/console/Formatter/OutputFormatter.php index 4ec600244..3e4897c33 100644 --- a/symfony/console/Formatter/OutputFormatter.php +++ b/symfony/console/Formatter/OutputFormatter.php @@ -23,9 +23,9 @@ */ class OutputFormatter implements WrappableOutputFormatterInterface { - private $decorated; - private $styles = []; - private $styleStack; + private bool $decorated; + private array $styles = []; + private OutputFormatterStyleStack $styleStack; public function __clone() { @@ -37,10 +37,8 @@ public function __clone() /** * Escapes "<" and ">" special chars in given text. - * - * @return string */ - public static function escape(string $text) + public static function escape(string $text): string { $text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text); @@ -86,41 +84,32 @@ public function __construct(bool $decorated = false, array $styles = []) } /** - * {@inheritdoc} + * @return void */ public function setDecorated(bool $decorated) { $this->decorated = $decorated; } - /** - * {@inheritdoc} - */ - public function isDecorated() + public function isDecorated(): bool { return $this->decorated; } /** - * {@inheritdoc} + * @return void */ public function setStyle(string $name, OutputFormatterStyleInterface $style) { $this->styles[strtolower($name)] = $style; } - /** - * {@inheritdoc} - */ - public function hasStyle(string $name) + public function hasStyle(string $name): bool { return isset($this->styles[strtolower($name)]); } - /** - * {@inheritdoc} - */ - public function getStyle(string $name) + public function getStyle(string $name): OutputFormatterStyleInterface { if (!$this->hasStyle($name)) { throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name)); @@ -129,16 +118,13 @@ public function getStyle(string $name) return $this->styles[strtolower($name)]; } - /** - * {@inheritdoc} - */ - public function format(?string $message) + public function format(?string $message): ?string { return $this->formatAndWrap($message, 0); } /** - * {@inheritdoc} + * @return string */ public function formatAndWrap(?string $message, int $width) { @@ -165,7 +151,7 @@ public function formatAndWrap(?string $message, int $width) $offset = $pos + \strlen($text); // opening tag? - if ($open = '/' != $text[1]) { + if ($open = '/' !== $text[1]) { $tag = $matches[1][$i][0]; } else { $tag = $matches[3][$i][0] ?? ''; @@ -188,10 +174,7 @@ public function formatAndWrap(?string $message, int $width) return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); } - /** - * @return OutputFormatterStyleStack - */ - public function getStyleStack() + public function getStyleStack(): OutputFormatterStyleStack { return $this->styleStack; } @@ -263,7 +246,7 @@ private function applyCurrentStyle(string $text, string $current, int $width, in $text = $prefix.$this->addLineBreaks($text, $width); $text = rtrim($text, "\n").($matches[1] ?? ''); - if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) { + if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) { $text = "\n".$text; } diff --git a/symfony/console/Formatter/OutputFormatterInterface.php b/symfony/console/Formatter/OutputFormatterInterface.php index 0b5f839a2..433cd4197 100644 --- a/symfony/console/Formatter/OutputFormatterInterface.php +++ b/symfony/console/Formatter/OutputFormatterInterface.php @@ -20,41 +20,37 @@ interface OutputFormatterInterface { /** * Sets the decorated flag. + * + * @return void */ public function setDecorated(bool $decorated); /** * Whether the output will decorate messages. - * - * @return bool */ - public function isDecorated(); + public function isDecorated(): bool; /** * Sets a new style. + * + * @return void */ public function setStyle(string $name, OutputFormatterStyleInterface $style); /** * Checks if output formatter has style with specified name. - * - * @return bool */ - public function hasStyle(string $name); + public function hasStyle(string $name): bool; /** * Gets style options from style with specified name. * - * @return OutputFormatterStyleInterface - * * @throws \InvalidArgumentException When style isn't defined */ - public function getStyle(string $name); + public function getStyle(string $name): OutputFormatterStyleInterface; /** * Formats a message according to the given styles. - * - * @return string|null */ - public function format(?string $message); + public function format(?string $message): ?string; } diff --git a/symfony/console/Formatter/OutputFormatterStyle.php b/symfony/console/Formatter/OutputFormatterStyle.php index d7ae66494..21e7f5ab0 100644 --- a/symfony/console/Formatter/OutputFormatterStyle.php +++ b/symfony/console/Formatter/OutputFormatterStyle.php @@ -20,12 +20,12 @@ */ class OutputFormatterStyle implements OutputFormatterStyleInterface { - private $color; - private $foreground; - private $background; - private $options; - private $href; - private $handlesHrefGracefully; + private Color $color; + private string $foreground; + private string $background; + private array $options; + private ?string $href = null; + private bool $handlesHrefGracefully; /** * Initializes output formatter style. @@ -39,18 +39,24 @@ public function __construct(?string $foreground = null, ?string $background = nu } /** - * {@inheritdoc} + * @return void */ public function setForeground(?string $color = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); } /** - * {@inheritdoc} + * @return void */ public function setBackground(?string $color = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); } @@ -60,7 +66,7 @@ public function setHref(string $url): void } /** - * {@inheritdoc} + * @return void */ public function setOption(string $option) { @@ -69,7 +75,7 @@ public function setOption(string $option) } /** - * {@inheritdoc} + * @return void */ public function unsetOption(string $option) { @@ -82,23 +88,18 @@ public function unsetOption(string $option) } /** - * {@inheritdoc} + * @return void */ public function setOptions(array $options) { $this->color = new Color($this->foreground, $this->background, $this->options = $options); } - /** - * {@inheritdoc} - */ - public function apply(string $text) + public function apply(string $text): string { - if (null === $this->handlesHrefGracefully) { - $this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') - && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100) - && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']); - } + $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') + && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100) + && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']); if (null !== $this->href && $this->handlesHrefGracefully) { $text = "\033]8;;$this->href\033\\$text\033]8;;\033\\"; diff --git a/symfony/console/Formatter/OutputFormatterStyleInterface.php b/symfony/console/Formatter/OutputFormatterStyleInterface.php index 89e4d2438..3b15098cb 100644 --- a/symfony/console/Formatter/OutputFormatterStyleInterface.php +++ b/symfony/console/Formatter/OutputFormatterStyleInterface.php @@ -20,33 +20,41 @@ interface OutputFormatterStyleInterface { /** * Sets style foreground color. + * + * @return void */ - public function setForeground(?string $color = null); + public function setForeground(?string $color); /** * Sets style background color. + * + * @return void */ - public function setBackground(?string $color = null); + public function setBackground(?string $color); /** * Sets some specific style option. + * + * @return void */ public function setOption(string $option); /** * Unsets some specific style option. + * + * @return void */ public function unsetOption(string $option); /** * Sets multiple style options at once. + * + * @return void */ public function setOptions(array $options); /** * Applies the style to a given text. - * - * @return string */ - public function apply(string $text); + public function apply(string $text): string; } diff --git a/symfony/console/Formatter/OutputFormatterStyleStack.php b/symfony/console/Formatter/OutputFormatterStyleStack.php index 1b9356301..62d2ca0e7 100644 --- a/symfony/console/Formatter/OutputFormatterStyleStack.php +++ b/symfony/console/Formatter/OutputFormatterStyleStack.php @@ -22,9 +22,9 @@ class OutputFormatterStyleStack implements ResetInterface /** * @var OutputFormatterStyleInterface[] */ - private $styles; + private array $styles = []; - private $emptyStyle; + private OutputFormatterStyleInterface $emptyStyle; public function __construct(?OutputFormatterStyleInterface $emptyStyle = null) { @@ -34,6 +34,8 @@ public function __construct(?OutputFormatterStyleInterface $emptyStyle = null) /** * Resets stack (ie. empty internal arrays). + * + * @return void */ public function reset() { @@ -42,6 +44,8 @@ public function reset() /** * Pushes a style in the stack. + * + * @return void */ public function push(OutputFormatterStyleInterface $style) { @@ -51,13 +55,11 @@ public function push(OutputFormatterStyleInterface $style) /** * Pops a style from the stack. * - * @return OutputFormatterStyleInterface - * * @throws InvalidArgumentException When style tags incorrectly nested */ - public function pop(?OutputFormatterStyleInterface $style = null) + public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface { - if (empty($this->styles)) { + if (!$this->styles) { return $this->emptyStyle; } @@ -78,12 +80,10 @@ public function pop(?OutputFormatterStyleInterface $style = null) /** * Computes current style with stacks top codes. - * - * @return OutputFormatterStyle */ - public function getCurrent() + public function getCurrent(): OutputFormatterStyleInterface { - if (empty($this->styles)) { + if (!$this->styles) { return $this->emptyStyle; } @@ -93,17 +93,14 @@ public function getCurrent() /** * @return $this */ - public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle) + public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle): static { $this->emptyStyle = $emptyStyle; return $this; } - /** - * @return OutputFormatterStyleInterface - */ - public function getEmptyStyle() + public function getEmptyStyle(): OutputFormatterStyleInterface { return $this->emptyStyle; } diff --git a/symfony/console/Formatter/WrappableOutputFormatterInterface.php b/symfony/console/Formatter/WrappableOutputFormatterInterface.php index 42319ee55..746cd27e7 100644 --- a/symfony/console/Formatter/WrappableOutputFormatterInterface.php +++ b/symfony/console/Formatter/WrappableOutputFormatterInterface.php @@ -20,6 +20,8 @@ interface WrappableOutputFormatterInterface extends OutputFormatterInterface { /** * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping). + * + * @return string */ public function formatAndWrap(?string $message, int $width); } diff --git a/symfony/console/Helper/DebugFormatterHelper.php b/symfony/console/Helper/DebugFormatterHelper.php index e258ba050..9ea7fb914 100644 --- a/symfony/console/Helper/DebugFormatterHelper.php +++ b/symfony/console/Helper/DebugFormatterHelper.php @@ -21,15 +21,13 @@ class DebugFormatterHelper extends Helper { private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default']; - private $started = []; - private $count = -1; + private array $started = []; + private int $count = -1; /** * Starts a debug formatting session. - * - * @return string */ - public function start(string $id, string $message, string $prefix = 'RUN') + public function start(string $id, string $message, string $prefix = 'RUN'): string { $this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)]; @@ -38,10 +36,8 @@ public function start(string $id, string $message, string $prefix = 'RUN') /** * Adds progress to a formatting session. - * - * @return string */ - public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR') + public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR'): string { $message = ''; @@ -74,10 +70,8 @@ public function progress(string $id, string $buffer, bool $error = false, string /** * Stops a formatting session. - * - * @return string */ - public function stop(string $id, string $message, bool $successful, string $prefix = 'RES') + public function stop(string $id, string $message, bool $successful, string $prefix = 'RES'): string { $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; @@ -97,10 +91,7 @@ private function getBorder(string $id): string return sprintf(' ', self::COLORS[$this->started[$id]['border']]); } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return 'debug_formatter'; } diff --git a/symfony/console/Helper/DescriptorHelper.php b/symfony/console/Helper/DescriptorHelper.php index af85e9c0a..eb32bce8f 100644 --- a/symfony/console/Helper/DescriptorHelper.php +++ b/symfony/console/Helper/DescriptorHelper.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Descriptor\DescriptorInterface; use Symfony\Component\Console\Descriptor\JsonDescriptor; use Symfony\Component\Console\Descriptor\MarkdownDescriptor; +use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor; use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Exception\InvalidArgumentException; @@ -29,7 +30,7 @@ class DescriptorHelper extends Helper /** * @var DescriptorInterface[] */ - private $descriptors = []; + private array $descriptors = []; public function __construct() { @@ -38,6 +39,7 @@ public function __construct() ->register('xml', new XmlDescriptor()) ->register('json', new JsonDescriptor()) ->register('md', new MarkdownDescriptor()) + ->register('rst', new ReStructuredTextDescriptor()) ; } @@ -48,6 +50,8 @@ public function __construct() * * format: string, the output format name * * raw_text: boolean, sets output type as raw * + * @return void + * * @throws InvalidArgumentException when the given format is not supported */ public function describe(OutputInterface $output, ?object $object, array $options = []) @@ -70,17 +74,14 @@ public function describe(OutputInterface $output, ?object $object, array $option * * @return $this */ - public function register(string $format, DescriptorInterface $descriptor) + public function register(string $format, DescriptorInterface $descriptor): static { $this->descriptors[$format] = $descriptor; return $this; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return 'descriptor'; } diff --git a/symfony/console/Helper/Dumper.php b/symfony/console/Helper/Dumper.php index 605e4d70b..a3b8e3952 100644 --- a/symfony/console/Helper/Dumper.php +++ b/symfony/console/Helper/Dumper.php @@ -21,10 +21,10 @@ */ final class Dumper { - private $output; - private $dumper; - private $cloner; - private $handler; + private OutputInterface $output; + private ?CliDumper $dumper; + private ?ClonerInterface $cloner; + private \Closure $handler; public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null) { @@ -34,30 +34,23 @@ public function __construct(OutputInterface $output, ?CliDumper $dumper = null, if (class_exists(CliDumper::class)) { $this->handler = function ($var): string { - $dumper = $this->dumper ?? $this->dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); + $dumper = $this->dumper ??= new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); $dumper->setColors($this->output->isDecorated()); - return rtrim($dumper->dump(($this->cloner ?? $this->cloner = new VarCloner())->cloneVar($var)->withRefHandles(false), true)); + return rtrim($dumper->dump(($this->cloner ??= new VarCloner())->cloneVar($var)->withRefHandles(false), true)); }; } else { - $this->handler = function ($var): string { - switch (true) { - case null === $var: - return 'null'; - case true === $var: - return 'true'; - case false === $var: - return 'false'; - case \is_string($var): - return '"'.$var.'"'; - default: - return rtrim(print_r($var, true)); - } + $this->handler = fn ($var): string => match (true) { + null === $var => 'null', + true === $var => 'true', + false === $var => 'false', + \is_string($var) => '"'.$var.'"', + default => rtrim(print_r($var, true)), }; } } - public function __invoke($var): string + public function __invoke(mixed $var): string { return ($this->handler)($var); } diff --git a/symfony/console/Helper/FormatterHelper.php b/symfony/console/Helper/FormatterHelper.php index 92d8dc724..279e4c799 100644 --- a/symfony/console/Helper/FormatterHelper.php +++ b/symfony/console/Helper/FormatterHelper.php @@ -22,22 +22,16 @@ class FormatterHelper extends Helper { /** * Formats a message within a section. - * - * @return string */ - public function formatSection(string $section, string $message, string $style = 'info') + public function formatSection(string $section, string $message, string $style = 'info'): string { return sprintf('<%s>[%s] %s', $style, $section, $style, $message); } /** * Formats a message as a block of text. - * - * @param string|array $messages The message to write in the block - * - * @return string */ - public function formatBlock($messages, string $style, bool $large = false) + public function formatBlock(string|array $messages, string $style, bool $large = false): string { if (!\is_array($messages)) { $messages = [$messages]; @@ -68,10 +62,8 @@ public function formatBlock($messages, string $style, bool $large = false) /** * Truncates a message to the given length. - * - * @return string */ - public function truncate(string $message, int $length, string $suffix = '...') + public function truncate(string $message, int $length, string $suffix = '...'): string { $computedLength = $length - self::width($suffix); @@ -82,10 +74,7 @@ public function truncate(string $message, int $length, string $suffix = '...') return self::substr($message, 0, $length).$suffix; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return 'formatter'; } diff --git a/symfony/console/Helper/Helper.php b/symfony/console/Helper/Helper.php index 6b3f7f43a..05be64787 100644 --- a/symfony/console/Helper/Helper.php +++ b/symfony/console/Helper/Helper.php @@ -21,45 +21,31 @@ */ abstract class Helper implements HelperInterface { - protected $helperSet = null; + protected $helperSet; /** - * {@inheritdoc} + * @return void */ public function setHelperSet(?HelperSet $helperSet = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->helperSet = $helperSet; } - /** - * {@inheritdoc} - */ - public function getHelperSet() + public function getHelperSet(): ?HelperSet { return $this->helperSet; } - /** - * Returns the length of a string, using mb_strwidth if it is available. - * - * @deprecated since Symfony 5.3 - * - * @return int - */ - public static function strlen(?string $string) - { - trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__); - - return self::width($string); - } - /** * Returns the width of a string, using mb_strwidth if it is available. * The width is how many characters positions the string will use. */ public static function width(?string $string): int { - $string ?? $string = ''; + $string ??= ''; if (preg_match('//u', $string)) { return (new UnicodeString($string))->width(false); @@ -78,7 +64,7 @@ public static function width(?string $string): int */ public static function length(?string $string): int { - $string ?? $string = ''; + $string ??= ''; if (preg_match('//u', $string)) { return (new UnicodeString($string))->length(); @@ -93,12 +79,10 @@ public static function length(?string $string): int /** * Returns the subset of a string, using mb_substr if it is available. - * - * @return string */ - public static function substr(?string $string, int $from, ?int $length = null) + public static function substr(?string $string, int $from, ?int $length = null): string { - $string ?? $string = ''; + $string ??= ''; if (false === $encoding = mb_detect_encoding($string, null, true)) { return substr($string, $from, $length); @@ -107,35 +91,52 @@ public static function substr(?string $string, int $from, ?int $length = null) return mb_substr($string, $from, $length, $encoding); } - public static function formatTime($secs) + /** + * @return string + */ + public static function formatTime(int|float $secs, int $precision = 1) { + $secs = (int) floor($secs); + + if (0 === $secs) { + return '< 1 sec'; + } + static $timeFormats = [ - [0, '< 1 sec'], - [1, '1 sec'], - [2, 'secs', 1], - [60, '1 min'], - [120, 'mins', 60], - [3600, '1 hr'], - [7200, 'hrs', 3600], - [86400, '1 day'], - [172800, 'days', 86400], + [1, '1 sec', 'secs'], + [60, '1 min', 'mins'], + [3600, '1 hr', 'hrs'], + [86400, '1 day', 'days'], ]; + $times = []; foreach ($timeFormats as $index => $format) { - if ($secs >= $format[0]) { - if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0]) - || $index == \count($timeFormats) - 1 - ) { - if (2 == \count($format)) { - return $format[1]; - } - - return floor($secs / $format[2]).' '.$format[1]; - } + $seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs; + + if (isset($times[$index - $precision])) { + unset($times[$index - $precision]); + } + + if (0 === $seconds) { + continue; + } + + $unitCount = ($seconds / $format[0]); + $times[$index] = 1 === $unitCount ? $format[1] : $unitCount.' '.$format[2]; + + if ($secs === $seconds) { + break; } + + $secs -= $seconds; } + + return implode(', ', array_reverse($times)); } + /** + * @return string + */ public static function formatMemory(int $memory) { if ($memory >= 1024 * 1024 * 1024) { @@ -154,15 +155,8 @@ public static function formatMemory(int $memory) } /** - * @deprecated since Symfony 5.3 + * @return string */ - public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string) - { - trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__); - - return self::width(self::removeDecoration($formatter, $string)); - } - public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string) { $isDecorated = $formatter->isDecorated(); diff --git a/symfony/console/Helper/HelperInterface.php b/symfony/console/Helper/HelperInterface.php index 5bf4d6327..ab626c938 100644 --- a/symfony/console/Helper/HelperInterface.php +++ b/symfony/console/Helper/HelperInterface.php @@ -20,15 +20,15 @@ interface HelperInterface { /** * Sets the helper set associated with this helper. + * + * @return void */ - public function setHelperSet(?HelperSet $helperSet = null); + public function setHelperSet(?HelperSet $helperSet); /** * Gets the helper set associated with this helper. - * - * @return HelperSet|null */ - public function getHelperSet(); + public function getHelperSet(): ?HelperSet; /** * Returns the canonical name of this helper. diff --git a/symfony/console/Helper/HelperSet.php b/symfony/console/Helper/HelperSet.php index c870ab997..f8c74ca2c 100644 --- a/symfony/console/Helper/HelperSet.php +++ b/symfony/console/Helper/HelperSet.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Console\Helper; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; /** @@ -19,16 +18,15 @@ * * @author Fabien Potencier * - * @implements \IteratorAggregate + * @implements \IteratorAggregate */ class HelperSet implements \IteratorAggregate { - /** @var array */ - private $helpers = []; - private $command; + /** @var array */ + private array $helpers = []; /** - * @param Helper[] $helpers An array of helper + * @param HelperInterface[] $helpers */ public function __construct(array $helpers = []) { @@ -37,6 +35,9 @@ public function __construct(array $helpers = []) } } + /** + * @return void + */ public function set(HelperInterface $helper, ?string $alias = null) { $this->helpers[$helper->getName()] = $helper; @@ -49,10 +50,8 @@ public function set(HelperInterface $helper, ?string $alias = null) /** * Returns true if the helper if defined. - * - * @return bool */ - public function has(string $name) + public function has(string $name): bool { return isset($this->helpers[$name]); } @@ -60,11 +59,9 @@ public function has(string $name) /** * Gets a helper value. * - * @return HelperInterface - * * @throws InvalidArgumentException if the helper is not defined */ - public function get(string $name) + public function get(string $name): HelperInterface { if (!$this->has($name)) { throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); @@ -73,35 +70,7 @@ public function get(string $name) return $this->helpers[$name]; } - /** - * @deprecated since Symfony 5.4 - */ - public function setCommand(?Command $command = null) - { - trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__); - - $this->command = $command; - } - - /** - * Gets the command associated with this helper set. - * - * @return Command - * - * @deprecated since Symfony 5.4 - */ - public function getCommand() - { - trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__); - - return $this->command; - } - - /** - * @return \Traversable - */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->helpers); } diff --git a/symfony/console/Helper/InputAwareHelper.php b/symfony/console/Helper/InputAwareHelper.php index 0d0dba23e..6f8225973 100644 --- a/symfony/console/Helper/InputAwareHelper.php +++ b/symfony/console/Helper/InputAwareHelper.php @@ -24,7 +24,7 @@ abstract class InputAwareHelper extends Helper implements InputAwareInterface protected $input; /** - * {@inheritdoc} + * @return void */ public function setInput(InputInterface $input) { diff --git a/symfony/console/Helper/OutputWrapper.php b/symfony/console/Helper/OutputWrapper.php new file mode 100644 index 000000000..2ec819c74 --- /dev/null +++ b/symfony/console/Helper/OutputWrapper.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Simple output wrapper for "tagged outputs" instead of wordwrap(). This solution is based on a StackOverflow + * answer: https://stackoverflow.com/a/20434776/1476819 from user557597 (alias SLN). + * + * (?: + * # -- Words/Characters + * ( # (1 start) + * (?> # Atomic Group - Match words with valid breaks + * .{1,16} # 1-N characters + * # Followed by one of 4 prioritized, non-linebreak whitespace + * (?: # break types: + * (?<= [^\S\r\n] ) # 1. - Behind a non-linebreak whitespace + * [^\S\r\n]? # ( optionally accept an extra non-linebreak whitespace ) + * | (?= \r? \n ) # 2. - Ahead a linebreak + * | $ # 3. - EOS + * | [^\S\r\n] # 4. - Accept an extra non-linebreak whitespace + * ) + * ) # End atomic group + * | + * .{1,16} # No valid word breaks, just break on the N'th character + * ) # (1 end) + * (?: \r? \n )? # Optional linebreak after Words/Characters + * | + * # -- Or, Linebreak + * (?: \r? \n | $ ) # Stand alone linebreak or at EOS + * ) + * + * @author Krisztián Ferenczi + * + * @see https://stackoverflow.com/a/20434776/1476819 + */ +final class OutputWrapper +{ + private const TAG_OPEN_REGEX_SEGMENT = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; + private const TAG_CLOSE_REGEX_SEGMENT = '[a-z][^<>]*+'; + private const URL_PATTERN = 'https?://\S+'; + + public function __construct( + private bool $allowCutUrls = false + ) { + } + + public function wrap(string $text, int $width, string $break = "\n"): string + { + if (!$width) { + return $text; + } + + $tagPattern = sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT); + $limitPattern = "{1,$width}"; + $patternBlocks = [$tagPattern]; + if (!$this->allowCutUrls) { + $patternBlocks[] = self::URL_PATTERN; + } + $patternBlocks[] = '.'; + $blocks = implode('|', $patternBlocks); + $rowPattern = "(?:$blocks)$limitPattern"; + $pattern = sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#imux', $rowPattern); + $output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break); + + return str_replace(' '.$break, $break, $output); + } +} diff --git a/symfony/console/Helper/ProcessHelper.php b/symfony/console/Helper/ProcessHelper.php index 86a250b27..3ef6f71f7 100644 --- a/symfony/console/Helper/ProcessHelper.php +++ b/symfony/console/Helper/ProcessHelper.php @@ -32,7 +32,7 @@ class ProcessHelper extends Helper * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR */ - public function run(OutputInterface $output, $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process + public function run(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process { if (!class_exists(Process::class)) { throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".'); @@ -48,10 +48,6 @@ public function run(OutputInterface $output, $cmd, ?string $error = null, ?calla $cmd = [$cmd]; } - if (!\is_array($cmd)) { - throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($cmd))); - } - if (\is_string($cmd[0] ?? null)) { $process = new Process($cmd); $cmd = []; @@ -98,7 +94,7 @@ public function run(OutputInterface $output, $cmd, ?string $error = null, ?calla * * @see run() */ - public function mustRun(OutputInterface $output, $cmd, ?string $error = null, ?callable $callback = null): Process + public function mustRun(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null): Process { $process = $this->run($output, $cmd, $error, $callback); @@ -134,9 +130,6 @@ private function escapeString(string $str): string return str_replace('<', '\\<', $str); } - /** - * {@inheritdoc} - */ public function getName(): string { return 'process'; diff --git a/symfony/console/Helper/ProgressBar.php b/symfony/console/Helper/ProgressBar.php index 1d7b8d456..b406292b4 100644 --- a/symfony/console/Helper/ProgressBar.php +++ b/symfony/console/Helper/ProgressBar.php @@ -36,31 +36,33 @@ final class ProgressBar private const FORMAT_DEBUG_NOMAX = 'debug_nomax'; private const FORMAT_NORMAL_NOMAX = 'normal_nomax'; - private $barWidth = 28; - private $barChar; - private $emptyBarChar = '-'; - private $progressChar = '>'; - private $format; - private $internalFormat; - private $redrawFreq = 1; - private $writeCount; - private $lastWriteTime; - private $minSecondsBetweenRedraws = 0; - private $maxSecondsBetweenRedraws = 1; - private $output; - private $step = 0; - private $max; - private $startTime; - private $stepWidth; - private $percent = 0.0; - private $messages = []; - private $overwrite = true; - private $terminal; - private $previousMessage; - private $cursor; - - private static $formatters; - private static $formats; + private int $barWidth = 28; + private string $barChar; + private string $emptyBarChar = '-'; + private string $progressChar = '>'; + private ?string $format = null; + private ?string $internalFormat = null; + private ?int $redrawFreq = 1; + private int $writeCount = 0; + private float $lastWriteTime = 0; + private float $minSecondsBetweenRedraws = 0; + private float $maxSecondsBetweenRedraws = 1; + private OutputInterface $output; + private int $step = 0; + private int $startingStep = 0; + private ?int $max = null; + private int $startTime; + private int $stepWidth; + private float $percent = 0.0; + private array $messages = []; + private bool $overwrite = true; + private Terminal $terminal; + private ?string $previousMessage = null; + private Cursor $cursor; + private array $placeholders = []; + + private static array $formatters; + private static array $formats; /** * @param int $max Maximum steps (0 if unknown) @@ -93,18 +95,16 @@ public function __construct(OutputInterface $output, int $max = 0, float $minSec } /** - * Sets a placeholder formatter for a given name. + * Sets a placeholder formatter for a given name, globally for all instances of ProgressBar. * * This method also allow you to override an existing placeholder. * - * @param string $name The placeholder name (including the delimiter char like %) - * @param callable $callable A PHP callable + * @param string $name The placeholder name (including the delimiter char like %) + * @param callable(ProgressBar):string $callable A PHP callable */ public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void { - if (!self::$formatters) { - self::$formatters = self::initPlaceholderFormatters(); - } + self::$formatters ??= self::initPlaceholderFormatters(); self::$formatters[$name] = $callable; } @@ -116,13 +116,31 @@ public static function setPlaceholderFormatterDefinition(string $name, callable */ public static function getPlaceholderFormatterDefinition(string $name): ?callable { - if (!self::$formatters) { - self::$formatters = self::initPlaceholderFormatters(); - } + self::$formatters ??= self::initPlaceholderFormatters(); return self::$formatters[$name] ?? null; } + /** + * Sets a placeholder formatter for a given name, for this instance only. + * + * @param callable(ProgressBar):string $callable A PHP callable + */ + public function setPlaceholderFormatter(string $name, callable $callable): void + { + $this->placeholders[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name. + * + * @param string $name The placeholder name (including the delimiter char like %) + */ + public function getPlaceholderFormatter(string $name): ?callable + { + return $this->placeholders[$name] ?? $this::getPlaceholderFormatterDefinition($name); + } + /** * Sets a format for a given name. * @@ -133,9 +151,7 @@ public static function getPlaceholderFormatterDefinition(string $name): ?callabl */ public static function setFormatDefinition(string $name, string $format): void { - if (!self::$formats) { - self::$formats = self::initFormats(); - } + self::$formats ??= self::initFormats(); self::$formats[$name] = $format; } @@ -147,9 +163,7 @@ public static function setFormatDefinition(string $name, string $format): void */ public static function getFormatDefinition(string $name): ?string { - if (!self::$formats) { - self::$formats = self::initFormats(); - } + self::$formats ??= self::initFormats(); return self::$formats[$name] ?? null; } @@ -164,14 +178,14 @@ public static function getFormatDefinition(string $name): ?string * @param string $message The text to associate with the placeholder * @param string $name The name of the placeholder */ - public function setMessage(string $message, string $name = 'message') + public function setMessage(string $message, string $name = 'message'): void { $this->messages[$name] = $message; } - public function getMessage(string $name = 'message') + public function getMessage(string $name = 'message'): ?string { - return $this->messages[$name]; + return $this->messages[$name] ?? null; } public function getStartTime(): int @@ -206,11 +220,11 @@ public function getBarOffset(): float public function getEstimated(): float { - if (!$this->step) { + if (0 === $this->step || $this->step === $this->startingStep) { return 0; } - return round((time() - $this->startTime) / $this->step * $this->max); + return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max); } public function getRemaining(): float @@ -219,10 +233,10 @@ public function getRemaining(): float return 0; } - return round((time() - $this->startTime) / $this->step * ($this->max - $this->step)); + return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step)); } - public function setBarWidth(int $size) + public function setBarWidth(int $size): void { $this->barWidth = max(1, $size); } @@ -232,7 +246,7 @@ public function getBarWidth(): int return $this->barWidth; } - public function setBarCharacter(string $char) + public function setBarCharacter(string $char): void { $this->barChar = $char; } @@ -242,7 +256,7 @@ public function getBarCharacter(): string return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar); } - public function setEmptyBarCharacter(string $char) + public function setEmptyBarCharacter(string $char): void { $this->emptyBarChar = $char; } @@ -252,7 +266,7 @@ public function getEmptyBarCharacter(): string return $this->emptyBarChar; } - public function setProgressCharacter(string $char) + public function setProgressCharacter(string $char): void { $this->progressChar = $char; } @@ -262,7 +276,7 @@ public function getProgressCharacter(): string return $this->progressChar; } - public function setFormat(string $format) + public function setFormat(string $format): void { $this->format = null; $this->internalFormat = $format; @@ -273,7 +287,7 @@ public function setFormat(string $format) * * @param int|null $freq The frequency in steps */ - public function setRedrawFrequency(?int $freq) + public function setRedrawFrequency(?int $freq): void { $this->redrawFreq = null !== $freq ? max(1, $freq) : null; } @@ -291,7 +305,13 @@ public function maxSecondsBetweenRedraws(float $seconds): void /** * Returns an iterator that will automatically update the progress bar when iterated. * - * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable + * @template TKey + * @template TValue + * + * @param iterable $iterable + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable + * + * @return iterable */ public function iterate(iterable $iterable, ?int $max = null): iterable { @@ -309,13 +329,16 @@ public function iterate(iterable $iterable, ?int $max = null): iterable /** * Starts the progress output. * - * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged + * @param int $startAt The starting point of the bar (useful e.g. when resuming a previously started bar) */ - public function start(?int $max = null) + public function start(?int $max = null, int $startAt = 0): void { $this->startTime = time(); - $this->step = 0; - $this->percent = 0.0; + $this->step = $startAt; + $this->startingStep = $startAt; + + $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0; if (null !== $max) { $this->setMaxSteps($max); @@ -329,7 +352,7 @@ public function start(?int $max = null) * * @param int $step Number of steps to advance */ - public function advance(int $step = 1) + public function advance(int $step = 1): void { $this->setProgress($this->step + $step); } @@ -337,12 +360,12 @@ public function advance(int $step = 1) /** * Sets whether to overwrite the progressbar, false for new line. */ - public function setOverwrite(bool $overwrite) + public function setOverwrite(bool $overwrite): void { $this->overwrite = $overwrite; } - public function setProgress(int $step) + public function setProgress(int $step): void { if ($this->max && $step > $this->max) { $this->max = $step; @@ -375,7 +398,7 @@ public function setProgress(int $step) } } - public function setMaxSteps(int $max) + public function setMaxSteps(int $max): void { $this->format = null; $this->max = max(0, $max); @@ -435,7 +458,7 @@ public function clear(): void $this->overwrite(''); } - private function setRealFormat(string $format) + private function setRealFormat(string $format): void { // try to use the _nomax variant if available if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { @@ -495,17 +518,13 @@ private function overwrite(string $message): void private function determineBestFormat(): string { - switch ($this->output->getVerbosity()) { + return match ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway - case OutputInterface::VERBOSITY_VERBOSE: - return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX; - case OutputInterface::VERBOSITY_VERY_VERBOSE: - return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX; - case OutputInterface::VERBOSITY_DEBUG: - return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX; - default: - return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX; - } + OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX, + OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX, + OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX, + default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX, + }; } private static function initPlaceholderFormatters(): array @@ -521,35 +540,25 @@ private static function initPlaceholderFormatters(): array return $display; }, - 'elapsed' => function (self $bar) { - return Helper::formatTime(time() - $bar->getStartTime()); - }, + 'elapsed' => fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2), 'remaining' => function (self $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } - return Helper::formatTime($bar->getRemaining()); + return Helper::formatTime($bar->getRemaining(), 2); }, 'estimated' => function (self $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } - return Helper::formatTime($bar->getEstimated()); - }, - 'memory' => function (self $bar) { - return Helper::formatMemory(memory_get_usage(true)); - }, - 'current' => function (self $bar) { - return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT); - }, - 'max' => function (self $bar) { - return $bar->getMaxSteps(); - }, - 'percent' => function (self $bar) { - return floor($bar->getProgressPercent() * 100); + return Helper::formatTime($bar->getEstimated(), 2); }, + 'memory' => fn (self $bar) => Helper::formatMemory(memory_get_usage(true)), + 'current' => fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT), + 'max' => fn (self $bar) => $bar->getMaxSteps(), + 'percent' => fn (self $bar) => floor($bar->getProgressPercent() * 100), ]; } @@ -572,9 +581,11 @@ private static function initFormats(): array private function buildLine(): string { + \assert(null !== $this->format); + $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; $callback = function ($matches) { - if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { + if ($formatter = $this->getPlaceholderFormatter($matches[1])) { $text = $formatter($this, $this->output); } elseif (isset($this->messages[$matches[1]])) { $text = $this->messages[$matches[1]]; @@ -591,9 +602,7 @@ private function buildLine(): string $line = preg_replace_callback($regex, $callback, $this->format); // gets string length for each sub line with multiline format - $linesLength = array_map(function ($subLine) { - return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))); - }, explode("\n", $line)); + $linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line)); $linesWidth = max($linesLength); diff --git a/symfony/console/Helper/ProgressIndicator.php b/symfony/console/Helper/ProgressIndicator.php index 3cc0e1451..92106caf6 100644 --- a/symfony/console/Helper/ProgressIndicator.php +++ b/symfony/console/Helper/ProgressIndicator.php @@ -31,20 +31,20 @@ class ProgressIndicator 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)', ]; - private $output; - private $startTime; - private $format; - private $message; - private $indicatorValues; - private $indicatorCurrent; - private $indicatorChangeInterval; - private $indicatorUpdateTime; - private $started = false; + private OutputInterface $output; + private int $startTime; + private ?string $format = null; + private ?string $message = null; + private array $indicatorValues; + private int $indicatorCurrent; + private int $indicatorChangeInterval; + private float $indicatorUpdateTime; + private bool $started = false; /** * @var array */ - private static $formatters; + private static array $formatters; /** * @param int $indicatorChangeInterval Change interval in milliseconds @@ -54,14 +54,8 @@ public function __construct(OutputInterface $output, ?string $format = null, int { $this->output = $output; - if (null === $format) { - $format = $this->determineBestFormat(); - } - - if (null === $indicatorValues) { - $indicatorValues = ['-', '\\', '|', '/']; - } - + $format ??= $this->determineBestFormat(); + $indicatorValues ??= ['-', '\\', '|', '/']; $indicatorValues = array_values($indicatorValues); if (2 > \count($indicatorValues)) { @@ -76,6 +70,8 @@ public function __construct(OutputInterface $output, ?string $format = null, int /** * Sets the current indicator message. + * + * @return void */ public function setMessage(?string $message) { @@ -86,6 +82,8 @@ public function setMessage(?string $message) /** * Starts the indicator output. + * + * @return void */ public function start(string $message) { @@ -104,6 +102,8 @@ public function start(string $message) /** * Advances the indicator. + * + * @return void */ public function advance() { @@ -129,6 +129,8 @@ public function advance() /** * Finish the indicator with message. + * + * @return void */ public function finish(string $message) { @@ -144,10 +146,8 @@ public function finish(string $message) /** * Gets the format for a given name. - * - * @return string|null */ - public static function getFormatDefinition(string $name) + public static function getFormatDefinition(string $name): ?string { return self::FORMATS[$name] ?? null; } @@ -156,31 +156,27 @@ public static function getFormatDefinition(string $name) * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. + * + * @return void */ public static function setPlaceholderFormatterDefinition(string $name, callable $callable) { - if (!self::$formatters) { - self::$formatters = self::initPlaceholderFormatters(); - } + self::$formatters ??= self::initPlaceholderFormatters(); self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name (including the delimiter char like %). - * - * @return callable|null */ - public static function getPlaceholderFormatterDefinition(string $name) + public static function getPlaceholderFormatterDefinition(string $name): ?callable { - if (!self::$formatters) { - self::$formatters = self::initPlaceholderFormatters(); - } + self::$formatters ??= self::initPlaceholderFormatters(); return self::$formatters[$name] ?? null; } - private function display() + private function display(): void { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; @@ -197,22 +193,19 @@ private function display() private function determineBestFormat(): string { - switch ($this->output->getVerbosity()) { + return match ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway - case OutputInterface::VERBOSITY_VERBOSE: - return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi'; - case OutputInterface::VERBOSITY_VERY_VERBOSE: - case OutputInterface::VERBOSITY_DEBUG: - return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi'; - default: - return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi'; - } + OutputInterface::VERBOSITY_VERBOSE => $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi', + OutputInterface::VERBOSITY_VERY_VERBOSE, + OutputInterface::VERBOSITY_DEBUG => $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi', + default => $this->output->isDecorated() ? 'normal' : 'normal_no_ansi', + }; } /** * Overwrites a previous message to the output. */ - private function overwrite(string $message) + private function overwrite(string $message): void { if ($this->output->isDecorated()) { $this->output->write("\x0D\x1B[2K"); @@ -227,21 +220,16 @@ private function getCurrentTimeInMilliseconds(): float return round(microtime(true) * 1000); } + /** + * @return array + */ private static function initPlaceholderFormatters(): array { return [ - 'indicator' => function (self $indicator) { - return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)]; - }, - 'message' => function (self $indicator) { - return $indicator->message; - }, - 'elapsed' => function (self $indicator) { - return Helper::formatTime(time() - $indicator->startTime); - }, - 'memory' => function () { - return Helper::formatMemory(memory_get_usage(true)); - }, + 'indicator' => fn (self $indicator) => $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)], + 'message' => fn (self $indicator) => $indicator->message, + 'elapsed' => fn (self $indicator) => Helper::formatTime(time() - $indicator->startTime, 2), + 'memory' => fn () => Helper::formatMemory(memory_get_usage(true)), ]; } } diff --git a/symfony/console/Helper/QuestionHelper.php b/symfony/console/Helper/QuestionHelper.php index e236be92a..b40b13191 100644 --- a/symfony/console/Helper/QuestionHelper.php +++ b/symfony/console/Helper/QuestionHelper.php @@ -39,8 +39,8 @@ class QuestionHelper extends Helper */ private $inputStream; - private static $stty = true; - private static $stdinIsInteractive; + private static bool $stty = true; + private static bool $stdinIsInteractive; /** * Asks a question to the user. @@ -49,7 +49,7 @@ class QuestionHelper extends Helper * * @throws RuntimeException If there is no data to read in the input stream */ - public function ask(InputInterface $input, OutputInterface $output, Question $question) + public function ask(InputInterface $input, OutputInterface $output, Question $question): mixed { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); @@ -68,9 +68,7 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu return $this->doAsk($output, $question); } - $interviewer = function () use ($output, $question) { - return $this->doAsk($output, $question); - }; + $interviewer = fn () => $this->doAsk($output, $question); return $this->validateAttempts($interviewer, $output, $question); } catch (MissingInputException $exception) { @@ -84,16 +82,15 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu } } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return 'question'; } /** * Prevents usage of stty. + * + * @return void */ public static function disableStty() { @@ -103,11 +100,9 @@ public static function disableStty() /** * Asks the question to the user. * - * @return mixed - * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ - private function doAsk(OutputInterface $output, Question $question) + private function doAsk(OutputInterface $output, Question $question): mixed { $this->writePrompt($output, $question); @@ -153,6 +148,7 @@ private function doAsk(OutputInterface $output, Question $question) } if ($output instanceof ConsoleSectionOutput) { + $output->addContent(''); // add EOL to the question $output->addContent($ret); } @@ -165,10 +161,7 @@ private function doAsk(OutputInterface $output, Question $question) return $ret; } - /** - * @return mixed - */ - private function getDefaultAnswer(Question $question) + private function getDefaultAnswer(Question $question): mixed { $default = $question->getDefault(); @@ -177,7 +170,7 @@ private function getDefaultAnswer(Question $question) } if ($validator = $question->getValidator()) { - return \call_user_func($question->getValidator(), $default); + return \call_user_func($validator, $default); } elseif ($question instanceof ChoiceQuestion) { $choices = $question->getChoices(); @@ -197,6 +190,8 @@ private function getDefaultAnswer(Question $question) /** * Outputs the question prompt. + * + * @return void */ protected function writePrompt(OutputInterface $output, Question $question) { @@ -216,7 +211,7 @@ protected function writePrompt(OutputInterface $output, Question $question) /** * @return string[] */ - protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag) + protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag): array { $messages = []; @@ -233,6 +228,8 @@ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string /** * Outputs an error message. + * + * @return void */ protected function writeError(OutputInterface $output, \Exception $error) { @@ -332,9 +329,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $matches = array_filter( $autocomplete($ret), - function ($match) use ($ret) { - return '' === $ret || str_starts_with($match, $ret); - } + fn ($match) => '' === $ret || str_starts_with($match, $ret) ); $numMatches = \count($matches); $ofs = -1; @@ -422,7 +417,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; // handle code running from a phar - if ('phar:' === substr(__FILE__, 0, 5)) { + if (str_starts_with(__FILE__, 'phar:')) { $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; copy($exe, $tmpExe); $exe = $tmpExe; @@ -448,6 +443,11 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $value = fgets($inputStream, 4096); + if (4095 === \strlen($value)) { + $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + $errOutput->warning('The value was possibly truncated by your shell or terminal emulator'); + } + if (self::$stty && Terminal::hasSttyAvailable()) { shell_exec('stty '.$sttyMode); } @@ -468,11 +468,9 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ * * @param callable $interviewer A callable that will ask for a question and return the result * - * @return mixed The validated response - * * @throws \Exception In case the max number of attempts has been reached and no valid response has been given */ - private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) + private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question): mixed { $error = null; $attempts = $question->getMaxAttempts(); @@ -499,23 +497,11 @@ private function isInteractiveInput($inputStream): bool return false; } - if (null !== self::$stdinIsInteractive) { + if (isset(self::$stdinIsInteractive)) { return self::$stdinIsInteractive; } - if (\function_exists('stream_isatty')) { - return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r')); - } - - if (\function_exists('posix_isatty')) { - return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r')); - } - - if (!\function_exists('shell_exec')) { - return self::$stdinIsInteractive = true; - } - - return self::$stdinIsInteractive = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); + return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r')); } /** @@ -523,10 +509,8 @@ private function isInteractiveInput($inputStream): bool * * @param resource $inputStream The handler resource * @param Question $question The question being asked - * - * @return string|false The input received, false in case input could not be read */ - private function readInput($inputStream, Question $question) + private function readInput($inputStream, Question $question): string|false { if (!$question->isMultiline()) { $cp = $this->setIOCodepage(); @@ -552,11 +536,6 @@ private function readInput($inputStream, Question $question) return $this->resetIOCodepage($cp, $ret); } - /** - * Sets console I/O to the host code page. - * - * @return int Previous code page in IBM/EBCDIC format - */ private function setIOCodepage(): int { if (\function_exists('sapi_windows_cp_set')) { @@ -571,12 +550,8 @@ private function setIOCodepage(): int /** * Sets console I/O to the specified code page and converts the user input. - * - * @param string|false $input - * - * @return string|false */ - private function resetIOCodepage(int $cp, $input) + private function resetIOCodepage(int $cp, string|false $input): string|false { if (0 !== $cp) { sapi_windows_cp_set($cp); diff --git a/symfony/console/Helper/SymfonyQuestionHelper.php b/symfony/console/Helper/SymfonyQuestionHelper.php index 01f94aba4..8ebc84376 100644 --- a/symfony/console/Helper/SymfonyQuestionHelper.php +++ b/symfony/console/Helper/SymfonyQuestionHelper.php @@ -26,7 +26,7 @@ class SymfonyQuestionHelper extends QuestionHelper { /** - * {@inheritdoc} + * @return void */ protected function writePrompt(OutputInterface $output, Question $question) { @@ -84,7 +84,7 @@ protected function writePrompt(OutputInterface $output, Question $question) } /** - * {@inheritdoc} + * @return void */ protected function writeError(OutputInterface $output, \Exception $error) { diff --git a/symfony/console/Helper/Table.php b/symfony/console/Helper/Table.php index 408a76d67..1f026dc50 100644 --- a/symfony/console/Helper/Table.php +++ b/symfony/console/Helper/Table.php @@ -35,112 +35,63 @@ class Table private const SEPARATOR_BOTTOM = 3; private const BORDER_OUTSIDE = 0; private const BORDER_INSIDE = 1; - - private $headerTitle; - private $footerTitle; - - /** - * Table headers. - */ - private $headers = []; - - /** - * Table rows. - */ - private $rows = []; - private $horizontal = false; - - /** - * Column widths cache. - */ - private $effectiveColumnWidths = []; - - /** - * Number of columns cache. - * - * @var int - */ - private $numberOfColumns; - - /** - * @var OutputInterface - */ - private $output; - - /** - * @var TableStyle - */ - private $style; - - /** - * @var array - */ - private $columnStyles = []; - - /** - * User set column widths. - * - * @var array - */ - private $columnWidths = []; - private $columnMaxWidths = []; - - /** - * @var array|null - */ - private static $styles; - - private $rendered = false; + private const DISPLAY_ORIENTATION_DEFAULT = 'default'; + private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal'; + private const DISPLAY_ORIENTATION_VERTICAL = 'vertical'; + + private ?string $headerTitle = null; + private ?string $footerTitle = null; + private array $headers = []; + private array $rows = []; + private array $effectiveColumnWidths = []; + private int $numberOfColumns; + private OutputInterface $output; + private TableStyle $style; + private array $columnStyles = []; + private array $columnWidths = []; + private array $columnMaxWidths = []; + private bool $rendered = false; + private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT; + + private static array $styles; public function __construct(OutputInterface $output) { $this->output = $output; - if (!self::$styles) { - self::$styles = self::initStyles(); - } + self::$styles ??= self::initStyles(); $this->setStyle('default'); } /** * Sets a style definition. + * + * @return void */ public static function setStyleDefinition(string $name, TableStyle $style) { - if (!self::$styles) { - self::$styles = self::initStyles(); - } + self::$styles ??= self::initStyles(); self::$styles[$name] = $style; } /** * Gets a style definition by name. - * - * @return TableStyle */ - public static function getStyleDefinition(string $name) + public static function getStyleDefinition(string $name): TableStyle { - if (!self::$styles) { - self::$styles = self::initStyles(); - } - - if (isset(self::$styles[$name])) { - return self::$styles[$name]; - } + self::$styles ??= self::initStyles(); - throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); + return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } /** * Sets table style. * - * @param TableStyle|string $name The style name or a TableStyle instance - * * @return $this */ - public function setStyle($name) + public function setStyle(TableStyle|string $name): static { $this->style = $this->resolveStyle($name); @@ -149,10 +100,8 @@ public function setStyle($name) /** * Gets the current table style. - * - * @return TableStyle */ - public function getStyle() + public function getStyle(): TableStyle { return $this->style; } @@ -164,7 +113,7 @@ public function getStyle() * * @return $this */ - public function setColumnStyle(int $columnIndex, $name) + public function setColumnStyle(int $columnIndex, TableStyle|string $name): static { $this->columnStyles[$columnIndex] = $this->resolveStyle($name); @@ -175,10 +124,8 @@ public function setColumnStyle(int $columnIndex, $name) * Gets the current style for a column. * * If style was not set, it returns the global table style. - * - * @return TableStyle */ - public function getColumnStyle(int $columnIndex) + public function getColumnStyle(int $columnIndex): TableStyle { return $this->columnStyles[$columnIndex] ?? $this->getStyle(); } @@ -188,7 +135,7 @@ public function getColumnStyle(int $columnIndex) * * @return $this */ - public function setColumnWidth(int $columnIndex, int $width) + public function setColumnWidth(int $columnIndex, int $width): static { $this->columnWidths[$columnIndex] = $width; @@ -200,7 +147,7 @@ public function setColumnWidth(int $columnIndex, int $width) * * @return $this */ - public function setColumnWidths(array $widths) + public function setColumnWidths(array $widths): static { $this->columnWidths = []; foreach ($widths as $index => $width) { @@ -218,7 +165,7 @@ public function setColumnWidths(array $widths) * * @return $this */ - public function setColumnMaxWidth(int $columnIndex, int $width): self + public function setColumnMaxWidth(int $columnIndex, int $width): static { if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter()))); @@ -232,10 +179,10 @@ public function setColumnMaxWidth(int $columnIndex, int $width): self /** * @return $this */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): static { $headers = array_values($headers); - if (!empty($headers) && !\is_array($headers[0])) { + if ($headers && !\is_array($headers[0])) { $headers = [$headers]; } @@ -244,6 +191,9 @@ public function setHeaders(array $headers) return $this; } + /** + * @return $this + */ public function setRows(array $rows) { $this->rows = []; @@ -254,7 +204,7 @@ public function setRows(array $rows) /** * @return $this */ - public function addRows(array $rows) + public function addRows(array $rows): static { foreach ($rows as $row) { $this->addRow($row); @@ -266,7 +216,7 @@ public function addRows(array $rows) /** * @return $this */ - public function addRow($row) + public function addRow(TableSeparator|array $row): static { if ($row instanceof TableSeparator) { $this->rows[] = $row; @@ -274,10 +224,6 @@ public function addRow($row) return $this; } - if (!\is_array($row)) { - throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); - } - $this->rows[] = array_values($row); return $this; @@ -288,7 +234,7 @@ public function addRow($row) * * @return $this */ - public function appendRow($row): self + public function appendRow(TableSeparator|array $row): static { if (!$this->output instanceof ConsoleSectionOutput) { throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); @@ -307,7 +253,7 @@ public function appendRow($row): self /** * @return $this */ - public function setRow($column, array $row) + public function setRow(int|string $column, array $row): static { $this->rows[$column] = $row; @@ -317,7 +263,7 @@ public function setRow($column, array $row) /** * @return $this */ - public function setHeaderTitle(?string $title): self + public function setHeaderTitle(?string $title): static { $this->headerTitle = $title; @@ -327,7 +273,7 @@ public function setHeaderTitle(?string $title): self /** * @return $this */ - public function setFooterTitle(?string $title): self + public function setFooterTitle(?string $title): static { $this->footerTitle = $title; @@ -337,9 +283,19 @@ public function setFooterTitle(?string $title): self /** * @return $this */ - public function setHorizontal(bool $horizontal = true): self + public function setHorizontal(bool $horizontal = true): static + { + $this->displayOrientation = $horizontal ? self::DISPLAY_ORIENTATION_HORIZONTAL : self::DISPLAY_ORIENTATION_DEFAULT; + + return $this; + } + + /** + * @return $this + */ + public function setVertical(bool $vertical = true): static { - $this->horizontal = $horizontal; + $this->displayOrientation = $vertical ? self::DISPLAY_ORIENTATION_VERTICAL : self::DISPLAY_ORIENTATION_DEFAULT; return $this; } @@ -356,12 +312,19 @@ public function setHorizontal(bool $horizontal = true): self * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | * +---------------+-----------------------+------------------+ + * + * @return void */ public function render() { $divider = new TableSeparator(); - if ($this->horizontal) { - $rows = []; + $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2; + + $horizontal = self::DISPLAY_ORIENTATION_HORIZONTAL === $this->displayOrientation; + $vertical = self::DISPLAY_ORIENTATION_VERTICAL === $this->displayOrientation; + + $rows = []; + if ($horizontal) { foreach ($this->headers[0] ?? [] as $i => $header) { $rows[$i] = [$header]; foreach ($this->rows as $row) { @@ -370,13 +333,62 @@ public function render() } if (isset($row[$i])) { $rows[$i][] = $row[$i]; - } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) { + } elseif ($isCellWithColspan($rows[$i][0])) { // Noop, there is a "title" } else { $rows[$i][] = null; } } } + } elseif ($vertical) { + $formatter = $this->output->getFormatter(); + $maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0); + + foreach ($this->rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + + if ($rows) { + $rows[] = [$divider]; + } + + $containsColspan = false; + foreach ($row as $cell) { + if ($containsColspan = $isCellWithColspan($cell)) { + break; + } + } + + $headers = $this->headers[0] ?? []; + $maxRows = max(\count($headers), \count($row)); + for ($i = 0; $i < $maxRows; ++$i) { + $cell = (string) ($row[$i] ?? ''); + + $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; + $parts = explode($eol, $cell); + foreach ($parts as $idx => $part) { + if ($headers && !$containsColspan) { + if (0 === $idx) { + $rows[] = [sprintf( + '%s%s: %s', + str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))), + $headers[$i] ?? '', + $part + )]; + } else { + $rows[] = [sprintf( + '%s %s', + str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT), + $part + )]; + } + } elseif ('' !== $cell) { + $rows[] = [$part]; + } + } + } + } } else { $rows = array_merge($this->headers, [$divider], $this->rows); } @@ -386,8 +398,8 @@ public function render() $rowGroups = $this->buildTableRows($rows); $this->calculateColumnsWidth($rowGroups); - $isHeader = !$this->horizontal; - $isFirstRow = $this->horizontal; + $isHeader = !$horizontal; + $isFirstRow = $horizontal; $hasTitle = (bool) $this->headerTitle; foreach ($rowGroups as $rowGroup) { @@ -413,7 +425,7 @@ public function render() if ($isHeader && !$isHeaderSeparatorRendered) { $this->renderRowSeparator( - $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, + self::SEPARATOR_TOP, $hasTitle ? $this->headerTitle : null, $hasTitle ? $this->style->getHeaderTitleFormat() : null ); @@ -423,7 +435,7 @@ public function render() if ($isFirstRow) { $this->renderRowSeparator( - $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, + $horizontal ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, $hasTitle ? $this->headerTitle : null, $hasTitle ? $this->style->getHeaderTitleFormat() : null ); @@ -431,7 +443,12 @@ public function render() $hasTitle = false; } - if ($this->horizontal) { + if ($vertical) { + $isHeader = false; + $isFirstRow = false; + } + + if ($horizontal) { $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat()); } else { $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); @@ -451,9 +468,9 @@ public function render() * * +-----+-----------+-------+ */ - private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null) + private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void { - if (0 === $count = $this->numberOfColumns) { + if (!$count = $this->numberOfColumns) { return; } @@ -516,7 +533,7 @@ private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string * * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | */ - private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null) + private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void { $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); $columns = $this->getRowColumns($row); @@ -570,11 +587,11 @@ private function renderCell(array $row, int $column, string $cellFormat): string $cellFormat = '<'.$tag.'>%s'; } - if (strstr($content, '')) { + if (str_contains($content, '')) { $content = str_replace('', '', $content); $width -= 3; } - if (strstr($content, '')) { + if (str_contains($content, '')) { $content = str_replace('', '', $content); $width -= \strlen(''); } @@ -589,7 +606,7 @@ private function renderCell(array $row, int $column, string $cellFormat): string /** * Calculate number of columns for this table. */ - private function calculateNumberOfColumns(array $rows) + private function calculateNumberOfColumns(array $rows): void { $columns = [0]; foreach ($rows as $row) { @@ -618,12 +635,13 @@ private function buildTableRows(array $rows): TableRows if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) { $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan); } - if (!strstr($cell ?? '', "\n")) { + if (!str_contains($cell ?? '', "\n")) { continue; } - $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell))); + $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n"; + $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell))); $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; - $lines = explode("\n", str_replace("\n", "\n", $cell)); + $lines = explode($eol, str_replace($eol, ''.$eol, $cell)); foreach ($lines as $lineKey => $line) { if ($colspan > 1) { $line = new TableCell($line, ['colspan' => $colspan]); @@ -662,7 +680,7 @@ private function calculateRowCount(): int ++$numberOfRows; // Add row for header separator } - if (\count($this->rows) > 0) { + if ($this->rows) { ++$numberOfRows; // Add row for footer separator } @@ -678,15 +696,16 @@ private function fillNextRows(array $rows, int $line): array { $unmergedRows = []; foreach ($rows[$line] as $column => $cell) { - if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { + if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) { throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell))); } if ($cell instanceof TableCell && $cell->getRowspan() > 1) { $nbLines = $cell->getRowspan() - 1; $lines = [$cell]; - if (strstr($cell, "\n")) { - $lines = explode("\n", str_replace("\n", "\n", $cell)); - $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; + if (str_contains($cell, "\n")) { + $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; + $lines = explode($eol, str_replace($eol, ''.$eol.'', $cell)); + $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines; $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); unset($lines[0]); @@ -728,7 +747,7 @@ private function fillNextRows(array $rows, int $line): array /** * fill cells for a row that contains colspan > 1. */ - private function fillCells(iterable $row) + private function fillCells(iterable $row): iterable { $newRow = []; @@ -790,7 +809,7 @@ private function getRowColumns(array $row): array /** * Calculates columns widths. */ - private function calculateColumnsWidth(iterable $groups) + private function calculateColumnsWidth(iterable $groups): void { for ($column = 0; $column < $this->numberOfColumns; ++$column) { $lengths = []; @@ -844,10 +863,10 @@ private function getCellWidth(array $row, int $column): int /** * Called after rendering to cleanup cache data. */ - private function cleanup() + private function cleanup(): void { $this->effectiveColumnWidths = []; - $this->numberOfColumns = null; + unset($this->numberOfColumns); } /** @@ -900,16 +919,12 @@ private static function initStyles(): array ]; } - private function resolveStyle($name): TableStyle + private function resolveStyle(TableStyle|string $name): TableStyle { if ($name instanceof TableStyle) { return $name; } - if (isset(self::$styles[$name])) { - return self::$styles[$name]; - } - - throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); + return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } } diff --git a/symfony/console/Helper/TableCell.php b/symfony/console/Helper/TableCell.php index 1a7bc6ede..394b2bc95 100644 --- a/symfony/console/Helper/TableCell.php +++ b/symfony/console/Helper/TableCell.php @@ -18,8 +18,8 @@ */ class TableCell { - private $value; - private $options = [ + private string $value; + private array $options = [ 'rowspan' => 1, 'colspan' => 1, 'style' => null, @@ -43,30 +43,24 @@ public function __construct(string $value = '', array $options = []) /** * Returns the cell value. - * - * @return string */ - public function __toString() + public function __toString(): string { return $this->value; } /** * Gets number of colspan. - * - * @return int */ - public function getColspan() + public function getColspan(): int { return (int) $this->options['colspan']; } /** * Gets number of rowspan. - * - * @return int */ - public function getRowspan() + public function getRowspan(): int { return (int) $this->options['rowspan']; } diff --git a/symfony/console/Helper/TableCellStyle.php b/symfony/console/Helper/TableCellStyle.php index 19cd0ffc6..9419dcb40 100644 --- a/symfony/console/Helper/TableCellStyle.php +++ b/symfony/console/Helper/TableCellStyle.php @@ -32,7 +32,7 @@ class TableCellStyle 'right' => \STR_PAD_LEFT, ]; - private $options = [ + private array $options = [ 'fg' => 'default', 'bg' => 'default', 'options' => null, @@ -63,21 +63,16 @@ public function getOptions(): array * * @return string[] */ - public function getTagOptions() + public function getTagOptions(): array { return array_filter( $this->getOptions(), - function ($key) { - return \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]); - }, + fn ($key) => \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]), \ARRAY_FILTER_USE_KEY ); } - /** - * @return int - */ - public function getPadByAlign() + public function getPadByAlign(): int { return self::ALIGN_MAP[$this->getOptions()['align']]; } diff --git a/symfony/console/Helper/TableRows.php b/symfony/console/Helper/TableRows.php index cbc07d294..97d07726e 100644 --- a/symfony/console/Helper/TableRows.php +++ b/symfony/console/Helper/TableRows.php @@ -16,7 +16,7 @@ */ class TableRows implements \IteratorAggregate { - private $generator; + private \Closure $generator; public function __construct(\Closure $generator) { diff --git a/symfony/console/Helper/TableStyle.php b/symfony/console/Helper/TableStyle.php index 0643c79eb..be956c109 100644 --- a/symfony/console/Helper/TableStyle.php +++ b/symfony/console/Helper/TableStyle.php @@ -23,37 +23,37 @@ */ class TableStyle { - private $paddingChar = ' '; - private $horizontalOutsideBorderChar = '-'; - private $horizontalInsideBorderChar = '-'; - private $verticalOutsideBorderChar = '|'; - private $verticalInsideBorderChar = '|'; - private $crossingChar = '+'; - private $crossingTopRightChar = '+'; - private $crossingTopMidChar = '+'; - private $crossingTopLeftChar = '+'; - private $crossingMidRightChar = '+'; - private $crossingBottomRightChar = '+'; - private $crossingBottomMidChar = '+'; - private $crossingBottomLeftChar = '+'; - private $crossingMidLeftChar = '+'; - private $crossingTopLeftBottomChar = '+'; - private $crossingTopMidBottomChar = '+'; - private $crossingTopRightBottomChar = '+'; - private $headerTitleFormat = ' %s '; - private $footerTitleFormat = ' %s '; - private $cellHeaderFormat = '%s'; - private $cellRowFormat = '%s'; - private $cellRowContentFormat = ' %s '; - private $borderFormat = '%s'; - private $padType = \STR_PAD_RIGHT; + private string $paddingChar = ' '; + private string $horizontalOutsideBorderChar = '-'; + private string $horizontalInsideBorderChar = '-'; + private string $verticalOutsideBorderChar = '|'; + private string $verticalInsideBorderChar = '|'; + private string $crossingChar = '+'; + private string $crossingTopRightChar = '+'; + private string $crossingTopMidChar = '+'; + private string $crossingTopLeftChar = '+'; + private string $crossingMidRightChar = '+'; + private string $crossingBottomRightChar = '+'; + private string $crossingBottomMidChar = '+'; + private string $crossingBottomLeftChar = '+'; + private string $crossingMidLeftChar = '+'; + private string $crossingTopLeftBottomChar = '+'; + private string $crossingTopMidBottomChar = '+'; + private string $crossingTopRightBottomChar = '+'; + private string $headerTitleFormat = ' %s '; + private string $footerTitleFormat = ' %s '; + private string $cellHeaderFormat = '%s'; + private string $cellRowFormat = '%s'; + private string $cellRowContentFormat = ' %s '; + private string $borderFormat = '%s'; + private int $padType = \STR_PAD_RIGHT; /** * Sets padding character, used for cell padding. * * @return $this */ - public function setPaddingChar(string $paddingChar) + public function setPaddingChar(string $paddingChar): static { if (!$paddingChar) { throw new LogicException('The padding char must not be empty.'); @@ -66,10 +66,8 @@ public function setPaddingChar(string $paddingChar) /** * Gets padding character, used for cell padding. - * - * @return string */ - public function getPaddingChar() + public function getPaddingChar(): string { return $this->paddingChar; } @@ -90,7 +88,7 @@ public function getPaddingChar() * * @return $this */ - public function setHorizontalBorderChars(string $outside, ?string $inside = null): self + public function setHorizontalBorderChars(string $outside, ?string $inside = null): static { $this->horizontalOutsideBorderChar = $outside; $this->horizontalInsideBorderChar = $inside ?? $outside; @@ -115,7 +113,7 @@ public function setHorizontalBorderChars(string $outside, ?string $inside = null * * @return $this */ - public function setVerticalBorderChars(string $outside, ?string $inside = null): self + public function setVerticalBorderChars(string $outside, ?string $inside = null): static { $this->verticalOutsideBorderChar = $outside; $this->verticalInsideBorderChar = $inside ?? $outside; @@ -169,7 +167,7 @@ public function getBorderChars(): array * * @return $this */ - public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): self + public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): static { $this->crossingChar = $cross; $this->crossingTopLeftChar = $topLeft; @@ -199,10 +197,8 @@ public function setDefaultCrossingChar(string $char): self /** * Gets crossing character. - * - * @return string */ - public function getCrossingChar() + public function getCrossingChar(): string { return $this->crossingChar; } @@ -235,7 +231,7 @@ public function getCrossingChars(): array * * @return $this */ - public function setCellHeaderFormat(string $cellHeaderFormat) + public function setCellHeaderFormat(string $cellHeaderFormat): static { $this->cellHeaderFormat = $cellHeaderFormat; @@ -244,10 +240,8 @@ public function setCellHeaderFormat(string $cellHeaderFormat) /** * Gets header cell format. - * - * @return string */ - public function getCellHeaderFormat() + public function getCellHeaderFormat(): string { return $this->cellHeaderFormat; } @@ -257,7 +251,7 @@ public function getCellHeaderFormat() * * @return $this */ - public function setCellRowFormat(string $cellRowFormat) + public function setCellRowFormat(string $cellRowFormat): static { $this->cellRowFormat = $cellRowFormat; @@ -266,10 +260,8 @@ public function setCellRowFormat(string $cellRowFormat) /** * Gets row cell format. - * - * @return string */ - public function getCellRowFormat() + public function getCellRowFormat(): string { return $this->cellRowFormat; } @@ -279,7 +271,7 @@ public function getCellRowFormat() * * @return $this */ - public function setCellRowContentFormat(string $cellRowContentFormat) + public function setCellRowContentFormat(string $cellRowContentFormat): static { $this->cellRowContentFormat = $cellRowContentFormat; @@ -288,10 +280,8 @@ public function setCellRowContentFormat(string $cellRowContentFormat) /** * Gets row cell content format. - * - * @return string */ - public function getCellRowContentFormat() + public function getCellRowContentFormat(): string { return $this->cellRowContentFormat; } @@ -301,7 +291,7 @@ public function getCellRowContentFormat() * * @return $this */ - public function setBorderFormat(string $borderFormat) + public function setBorderFormat(string $borderFormat): static { $this->borderFormat = $borderFormat; @@ -310,10 +300,8 @@ public function setBorderFormat(string $borderFormat) /** * Gets table border format. - * - * @return string */ - public function getBorderFormat() + public function getBorderFormat(): string { return $this->borderFormat; } @@ -323,7 +311,7 @@ public function getBorderFormat() * * @return $this */ - public function setPadType(int $padType) + public function setPadType(int $padType): static { if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) { throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).'); @@ -336,10 +324,8 @@ public function setPadType(int $padType) /** * Gets cell padding type. - * - * @return int */ - public function getPadType() + public function getPadType(): int { return $this->padType; } @@ -352,7 +338,7 @@ public function getHeaderTitleFormat(): string /** * @return $this */ - public function setHeaderTitleFormat(string $format): self + public function setHeaderTitleFormat(string $format): static { $this->headerTitleFormat = $format; @@ -367,7 +353,7 @@ public function getFooterTitleFormat(): string /** * @return $this */ - public function setFooterTitleFormat(string $format): self + public function setFooterTitleFormat(string $format): static { $this->footerTitleFormat = $format; diff --git a/symfony/console/Input/ArgvInput.php b/symfony/console/Input/ArgvInput.php index 0c4b2d25b..ab9f28c54 100644 --- a/symfony/console/Input/ArgvInput.php +++ b/symfony/console/Input/ArgvInput.php @@ -40,12 +40,12 @@ */ class ArgvInput extends Input { - private $tokens; - private $parsed; + private array $tokens; + private array $parsed; public function __construct(?array $argv = null, ?InputDefinition $definition = null) { - $argv = $argv ?? $_SERVER['argv'] ?? []; + $argv ??= $_SERVER['argv'] ?? []; // strip the application name array_shift($argv); @@ -55,13 +55,16 @@ public function __construct(?array $argv = null, ?InputDefinition $definition = parent::__construct($definition); } + /** + * @return void + */ protected function setTokens(array $tokens) { $this->tokens = $tokens; } /** - * {@inheritdoc} + * @return void */ protected function parse() { @@ -92,7 +95,7 @@ protected function parseToken(string $token, bool $parseOptions): bool /** * Parses a short option. */ - private function parseShortOption(string $token) + private function parseShortOption(string $token): void { $name = substr($token, 1); @@ -113,7 +116,7 @@ private function parseShortOption(string $token) * * @throws RuntimeException When option given doesn't exist */ - private function parseShortOptionSet(string $name) + private function parseShortOptionSet(string $name): void { $len = \strlen($name); for ($i = 0; $i < $len; ++$i) { @@ -136,7 +139,7 @@ private function parseShortOptionSet(string $name) /** * Parses a long option. */ - private function parseLongOption(string $token) + private function parseLongOption(string $token): void { $name = substr($token, 2); @@ -155,7 +158,7 @@ private function parseLongOption(string $token) * * @throws RuntimeException When too many arguments are given */ - private function parseArgument(string $token) + private function parseArgument(string $token): void { $c = \count($this->arguments); @@ -199,7 +202,7 @@ private function parseArgument(string $token) * * @throws RuntimeException When option given doesn't exist */ - private function addShortOption(string $shortcut, $value) + private function addShortOption(string $shortcut, mixed $value): void { if (!$this->definition->hasShortcut($shortcut)) { throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); @@ -213,7 +216,7 @@ private function addShortOption(string $shortcut, $value) * * @throws RuntimeException When option given doesn't exist */ - private function addLongOption(string $name, $value) + private function addLongOption(string $name, mixed $value): void { if (!$this->definition->hasOption($name)) { if (!$this->definition->hasNegation($name)) { @@ -263,10 +266,7 @@ private function addLongOption(string $name, $value) } } - /** - * {@inheritdoc} - */ - public function getFirstArgument() + public function getFirstArgument(): ?string { $isOption = false; foreach ($this->tokens as $i => $token) { @@ -298,10 +298,7 @@ public function getFirstArgument() return null; } - /** - * {@inheritdoc} - */ - public function hasParameterOption($values, bool $onlyParams = false) + public function hasParameterOption(string|array $values, bool $onlyParams = false): bool { $values = (array) $values; @@ -323,10 +320,7 @@ public function hasParameterOption($values, bool $onlyParams = false) return false; } - /** - * {@inheritdoc} - */ - public function getParameterOption($values, $default = false, bool $onlyParams = false) + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed { $values = (array) $values; $tokens = $this->tokens; @@ -356,10 +350,8 @@ public function getParameterOption($values, $default = false, bool $onlyParams = /** * Returns a stringified representation of the args passed to the command. - * - * @return string */ - public function __toString() + public function __toString(): string { $tokens = array_map(function ($token) { if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { diff --git a/symfony/console/Input/ArrayInput.php b/symfony/console/Input/ArrayInput.php index 21a517cfb..c1bc914ca 100644 --- a/symfony/console/Input/ArrayInput.php +++ b/symfony/console/Input/ArrayInput.php @@ -25,7 +25,7 @@ */ class ArrayInput extends Input { - private $parameters; + private array $parameters; public function __construct(array $parameters, ?InputDefinition $definition = null) { @@ -34,10 +34,7 @@ public function __construct(array $parameters, ?InputDefinition $definition = nu parent::__construct($definition); } - /** - * {@inheritdoc} - */ - public function getFirstArgument() + public function getFirstArgument(): ?string { foreach ($this->parameters as $param => $value) { if ($param && \is_string($param) && '-' === $param[0]) { @@ -50,10 +47,7 @@ public function getFirstArgument() return null; } - /** - * {@inheritdoc} - */ - public function hasParameterOption($values, bool $onlyParams = false) + public function hasParameterOption(string|array $values, bool $onlyParams = false): bool { $values = (array) $values; @@ -74,10 +68,7 @@ public function hasParameterOption($values, bool $onlyParams = false) return false; } - /** - * {@inheritdoc} - */ - public function getParameterOption($values, $default = false, bool $onlyParams = false) + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed { $values = (array) $values; @@ -100,10 +91,8 @@ public function getParameterOption($values, $default = false, bool $onlyParams = /** * Returns a stringified representation of the args passed to the command. - * - * @return string */ - public function __toString() + public function __toString(): string { $params = []; foreach ($this->parameters as $param => $val) { @@ -117,7 +106,7 @@ public function __toString() $params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : ''); } } else { - $params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val); + $params[] = \is_array($val) ? implode(' ', array_map($this->escapeToken(...), $val)) : $this->escapeToken($val); } } @@ -125,7 +114,7 @@ public function __toString() } /** - * {@inheritdoc} + * @return void */ protected function parse() { @@ -148,7 +137,7 @@ protected function parse() * * @throws InvalidOptionException When option given doesn't exist */ - private function addShortOption(string $shortcut, $value) + private function addShortOption(string $shortcut, mixed $value): void { if (!$this->definition->hasShortcut($shortcut)) { throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut)); @@ -163,7 +152,7 @@ private function addShortOption(string $shortcut, $value) * @throws InvalidOptionException When option given doesn't exist * @throws InvalidOptionException When a required value is missing */ - private function addLongOption(string $name, $value) + private function addLongOption(string $name, mixed $value): void { if (!$this->definition->hasOption($name)) { if (!$this->definition->hasNegation($name)) { @@ -194,12 +183,9 @@ private function addLongOption(string $name, $value) /** * Adds an argument value. * - * @param string|int $name The argument name - * @param mixed $value The value for the argument - * * @throws InvalidArgumentException When argument given doesn't exist */ - private function addArgument($name, $value) + private function addArgument(string|int $name, mixed $value): void { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); diff --git a/symfony/console/Input/Input.php b/symfony/console/Input/Input.php index 0faab2cf1..1c21573bc 100644 --- a/symfony/console/Input/Input.php +++ b/symfony/console/Input/Input.php @@ -28,6 +28,7 @@ abstract class Input implements InputInterface, StreamableInputInterface { protected $definition; + /** @var resource */ protected $stream; protected $options = []; protected $arguments = []; @@ -44,7 +45,7 @@ public function __construct(?InputDefinition $definition = null) } /** - * {@inheritdoc} + * @return void */ public function bind(InputDefinition $definition) { @@ -57,54 +58,45 @@ public function bind(InputDefinition $definition) /** * Processes command line arguments. + * + * @return void */ abstract protected function parse(); /** - * {@inheritdoc} + * @return void */ public function validate() { $definition = $this->definition; $givenArguments = $this->arguments; - $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { - return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); - }); + $missingArguments = array_filter(array_keys($definition->getArguments()), fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired()); if (\count($missingArguments) > 0) { throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))); } } - /** - * {@inheritdoc} - */ - public function isInteractive() + public function isInteractive(): bool { return $this->interactive; } /** - * {@inheritdoc} + * @return void */ public function setInteractive(bool $interactive) { $this->interactive = $interactive; } - /** - * {@inheritdoc} - */ - public function getArguments() + public function getArguments(): array { return array_merge($this->definition->getArgumentDefaults(), $this->arguments); } - /** - * {@inheritdoc} - */ - public function getArgument(string $name) + public function getArgument(string $name): mixed { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); @@ -114,9 +106,9 @@ public function getArgument(string $name) } /** - * {@inheritdoc} + * @return void */ - public function setArgument(string $name, $value) + public function setArgument(string $name, mixed $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); @@ -125,26 +117,17 @@ public function setArgument(string $name, $value) $this->arguments[$name] = $value; } - /** - * {@inheritdoc} - */ - public function hasArgument(string $name) + public function hasArgument(string $name): bool { return $this->definition->hasArgument($name); } - /** - * {@inheritdoc} - */ - public function getOptions() + public function getOptions(): array { return array_merge($this->definition->getOptionDefaults(), $this->options); } - /** - * {@inheritdoc} - */ - public function getOption(string $name) + public function getOption(string $name): mixed { if ($this->definition->hasNegation($name)) { if (null === $value = $this->getOption($this->definition->negationToName($name))) { @@ -162,9 +145,9 @@ public function getOption(string $name) } /** - * {@inheritdoc} + * @return void */ - public function setOption(string $name, $value) + public function setOption(string $name, mixed $value) { if ($this->definition->hasNegation($name)) { $this->options[$this->definition->negationToName($name)] = !$value; @@ -177,26 +160,23 @@ public function setOption(string $name, $value) $this->options[$name] = $value; } - /** - * {@inheritdoc} - */ - public function hasOption(string $name) + public function hasOption(string $name): bool { return $this->definition->hasOption($name) || $this->definition->hasNegation($name); } /** * Escapes a token through escapeshellarg if it contains unsafe chars. - * - * @return string */ - public function escapeToken(string $token) + public function escapeToken(string $token): string { return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); } /** - * {@inheritdoc} + * @param resource $stream + * + * @return void */ public function setStream($stream) { @@ -204,7 +184,7 @@ public function setStream($stream) } /** - * {@inheritdoc} + * @return resource */ public function getStream() { diff --git a/symfony/console/Input/InputArgument.php b/symfony/console/Input/InputArgument.php index 1a8bf44b7..4ef79feb7 100644 --- a/symfony/console/Input/InputArgument.php +++ b/symfony/console/Input/InputArgument.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Console\Input; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -25,20 +29,22 @@ class InputArgument public const OPTIONAL = 2; public const IS_ARRAY = 4; - private $name; - private $mode; - private $default; - private $description; + private string $name; + private int $mode; + private string|int|bool|array|null|float $default; + private array|\Closure $suggestedValues; + private string $description; /** - * @param string $name The argument name - * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY - * @param string $description A description text - * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) + * @param string $name The argument name + * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY + * @param string $description A description text + * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @throws InvalidArgumentException When argument mode is not valid */ - public function __construct(string $name, ?int $mode = null, string $description = '', $default = null) + public function __construct(string $name, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, \Closure|array $suggestedValues = []) { if (null === $mode) { $mode = self::OPTIONAL; @@ -49,16 +55,15 @@ public function __construct(string $name, ?int $mode = null, string $description $this->name = $name; $this->mode = $mode; $this->description = $description; + $this->suggestedValues = $suggestedValues; $this->setDefault($default); } /** * Returns the argument name. - * - * @return string */ - public function getName() + public function getName(): string { return $this->name; } @@ -68,7 +73,7 @@ public function getName() * * @return bool true if parameter mode is self::REQUIRED, false otherwise */ - public function isRequired() + public function isRequired(): bool { return self::REQUIRED === (self::REQUIRED & $this->mode); } @@ -78,7 +83,7 @@ public function isRequired() * * @return bool true if mode is self::IS_ARRAY, false otherwise */ - public function isArray() + public function isArray(): bool { return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); } @@ -86,12 +91,15 @@ public function isArray() /** * Sets the default value. * - * @param string|bool|int|float|array|null $default + * @return void * * @throws LogicException When incorrect default value is given */ - public function setDefault($default = null) + public function setDefault(string|bool|int|float|array|null $default = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if ($this->isRequired() && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } @@ -109,20 +117,37 @@ public function setDefault($default = null) /** * Returns the default value. - * - * @return string|bool|int|float|array|null */ - public function getDefault() + public function getDefault(): string|bool|int|float|array|null { return $this->default; } + public function hasCompletion(): bool + { + return [] !== $this->suggestedValues; + } + /** - * Returns the description text. + * Adds suggestions to $suggestions for the current completion input. * - * @return string + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } + + /** + * Returns the description text. */ - public function getDescription() + public function getDescription(): string { return $this->description; } diff --git a/symfony/console/Input/InputAwareInterface.php b/symfony/console/Input/InputAwareInterface.php index 5a288de5d..0ad27b455 100644 --- a/symfony/console/Input/InputAwareInterface.php +++ b/symfony/console/Input/InputAwareInterface.php @@ -21,6 +21,8 @@ interface InputAwareInterface { /** * Sets the Console Input. + * + * @return void */ public function setInput(InputInterface $input); } diff --git a/symfony/console/Input/InputDefinition.php b/symfony/console/Input/InputDefinition.php index 11f704f0e..b7162d770 100644 --- a/symfony/console/Input/InputDefinition.php +++ b/symfony/console/Input/InputDefinition.php @@ -28,13 +28,13 @@ */ class InputDefinition { - private $arguments; - private $requiredCount; - private $lastArrayArgument; - private $lastOptionalArgument; - private $options; - private $negations; - private $shortcuts; + private array $arguments = []; + private int $requiredCount = 0; + private ?InputArgument $lastArrayArgument = null; + private ?InputArgument $lastOptionalArgument = null; + private array $options = []; + private array $negations = []; + private array $shortcuts = []; /** * @param array $definition An array of InputArgument and InputOption instance @@ -46,6 +46,8 @@ public function __construct(array $definition = []) /** * Sets the definition of the input. + * + * @return void */ public function setDefinition(array $definition) { @@ -67,6 +69,8 @@ public function setDefinition(array $definition) * Sets the InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects + * + * @return void */ public function setArguments(array $arguments = []) { @@ -81,6 +85,8 @@ public function setArguments(array $arguments = []) * Adds an array of InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects + * + * @return void */ public function addArguments(?array $arguments = []) { @@ -92,6 +98,8 @@ public function addArguments(?array $arguments = []) } /** + * @return void + * * @throws LogicException When incorrect argument is given */ public function addArgument(InputArgument $argument) @@ -124,13 +132,9 @@ public function addArgument(InputArgument $argument) /** * Returns an InputArgument by name or by position. * - * @param string|int $name The InputArgument name or position - * - * @return InputArgument - * * @throws InvalidArgumentException When argument given doesn't exist */ - public function getArgument($name) + public function getArgument(string|int $name): InputArgument { if (!$this->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); @@ -143,12 +147,8 @@ public function getArgument($name) /** * Returns true if an InputArgument object exists by name or position. - * - * @param string|int $name The InputArgument name or position - * - * @return bool */ - public function hasArgument($name) + public function hasArgument(string|int $name): bool { $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; @@ -160,27 +160,23 @@ public function hasArgument($name) * * @return InputArgument[] */ - public function getArguments() + public function getArguments(): array { return $this->arguments; } /** * Returns the number of InputArguments. - * - * @return int */ - public function getArgumentCount() + public function getArgumentCount(): int { return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments); } /** * Returns the number of required InputArguments. - * - * @return int */ - public function getArgumentRequiredCount() + public function getArgumentRequiredCount(): int { return $this->requiredCount; } @@ -188,7 +184,7 @@ public function getArgumentRequiredCount() /** * @return array */ - public function getArgumentDefaults() + public function getArgumentDefaults(): array { $values = []; foreach ($this->arguments as $argument) { @@ -202,6 +198,8 @@ public function getArgumentDefaults() * Sets the InputOption objects. * * @param InputOption[] $options An array of InputOption objects + * + * @return void */ public function setOptions(array $options = []) { @@ -215,6 +213,8 @@ public function setOptions(array $options = []) * Adds an array of InputOption objects. * * @param InputOption[] $options An array of InputOption objects + * + * @return void */ public function addOptions(array $options = []) { @@ -224,6 +224,8 @@ public function addOptions(array $options = []) } /** + * @return void + * * @throws LogicException When option given already exist */ public function addOption(InputOption $option) @@ -262,11 +264,9 @@ public function addOption(InputOption $option) /** * Returns an InputOption by name. * - * @return InputOption - * * @throws InvalidArgumentException When option given doesn't exist */ - public function getOption(string $name) + public function getOption(string $name): InputOption { if (!$this->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); @@ -280,10 +280,8 @@ public function getOption(string $name) * * This method can't be used to check if the user included the option when * executing the command (use getOption() instead). - * - * @return bool */ - public function hasOption(string $name) + public function hasOption(string $name): bool { return isset($this->options[$name]); } @@ -293,17 +291,15 @@ public function hasOption(string $name) * * @return InputOption[] */ - public function getOptions() + public function getOptions(): array { return $this->options; } /** * Returns true if an InputOption object exists by shortcut. - * - * @return bool */ - public function hasShortcut(string $name) + public function hasShortcut(string $name): bool { return isset($this->shortcuts[$name]); } @@ -318,10 +314,8 @@ public function hasNegation(string $name): bool /** * Gets an InputOption by shortcut. - * - * @return InputOption */ - public function getOptionForShortcut(string $shortcut) + public function getOptionForShortcut(string $shortcut): InputOption { return $this->getOption($this->shortcutToName($shortcut)); } @@ -329,7 +323,7 @@ public function getOptionForShortcut(string $shortcut) /** * @return array */ - public function getOptionDefaults() + public function getOptionDefaults(): array { $values = []; foreach ($this->options as $option) { @@ -373,10 +367,8 @@ public function negationToName(string $negation): string /** * Gets the synopsis. - * - * @return string */ - public function getSynopsis(bool $short = false) + public function getSynopsis(bool $short = false): string { $elements = []; diff --git a/symfony/console/Input/InputInterface.php b/symfony/console/Input/InputInterface.php index 628b6037a..aaed5fd01 100644 --- a/symfony/console/Input/InputInterface.php +++ b/symfony/console/Input/InputInterface.php @@ -18,15 +18,16 @@ * InputInterface is the interface implemented by all input classes. * * @author Fabien Potencier + * + * @method string __toString() Returns a stringified representation of the args passed to the command. + * InputArguments MUST be escaped as well as the InputOption values passed to the command. */ interface InputInterface { /** * Returns the first argument from the raw parameters (not parsed). - * - * @return string|null */ - public function getFirstArgument(); + public function getFirstArgument(): ?string; /** * Returns true if the raw parameters (not parsed) contain a value. @@ -38,10 +39,8 @@ public function getFirstArgument(); * * @param string|array $values The values to look for in the raw parameters (can be an array) * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal - * - * @return bool */ - public function hasParameterOption($values, bool $onlyParams = false); + public function hasParameterOption(string|array $values, bool $onlyParams = false): bool; /** * Returns the value of a raw option (not parsed). @@ -57,11 +56,13 @@ public function hasParameterOption($values, bool $onlyParams = false); * * @return mixed */ - public function getParameterOption($values, $default = false, bool $onlyParams = false); + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false); /** * Binds the current Input instance with the given arguments and options. * + * @return void + * * @throws RuntimeException */ public function bind(InputDefinition $definition); @@ -69,6 +70,8 @@ public function bind(InputDefinition $definition); /** * Validates the input. * + * @return void + * * @throws RuntimeException When not enough arguments are given */ public function validate(); @@ -78,7 +81,7 @@ public function validate(); * * @return array */ - public function getArguments(); + public function getArguments(): array; /** * Returns the argument value for a given argument name. @@ -92,25 +95,23 @@ public function getArgument(string $name); /** * Sets an argument value by name. * - * @param mixed $value The argument value + * @return void * * @throws InvalidArgumentException When argument given doesn't exist */ - public function setArgument(string $name, $value); + public function setArgument(string $name, mixed $value); /** * Returns true if an InputArgument object exists by name or position. - * - * @return bool */ - public function hasArgument(string $name); + public function hasArgument(string $name): bool; /** * Returns all the given options merged with the default values. * * @return array */ - public function getOptions(); + public function getOptions(): array; /** * Returns the option value for a given option name. @@ -124,28 +125,26 @@ public function getOption(string $name); /** * Sets an option value by name. * - * @param mixed $value The option value + * @return void * * @throws InvalidArgumentException When option given doesn't exist */ - public function setOption(string $name, $value); + public function setOption(string $name, mixed $value); /** * Returns true if an InputOption object exists by name. - * - * @return bool */ - public function hasOption(string $name); + public function hasOption(string $name): bool; /** * Is this input means interactive? - * - * @return bool */ - public function isInteractive(); + public function isInteractive(): bool; /** * Sets the input interactivity. + * + * @return void */ public function setInteractive(bool $interactive); } diff --git a/symfony/console/Input/InputOption.php b/symfony/console/Input/InputOption.php index 1d8dbca31..bb533801f 100644 --- a/symfony/console/Input/InputOption.php +++ b/symfony/console/Input/InputOption.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Console\Input; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -46,20 +50,22 @@ class InputOption */ public const VALUE_NEGATABLE = 16; - private $name; - private $shortcut; - private $mode; - private $default; - private $description; + private string $name; + private string|array|null $shortcut; + private int $mode; + private string|int|bool|array|null|float $default; + private array|\Closure $suggestedValues; + private string $description; /** - * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts - * @param int|null $mode The option mode: One of the VALUE_* constants - * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE) + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the VALUE_* constants + * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @throws InvalidArgumentException If option mode is invalid or incompatible */ - public function __construct(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null) + public function __construct(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, array|\Closure $suggestedValues = []) { if (str_starts_with($name, '--')) { $name = substr($name, 2); @@ -69,7 +75,7 @@ public function __construct(string $name, $shortcut = null, ?int $mode = null, s throw new InvalidArgumentException('An option name cannot be empty.'); } - if ('' === $shortcut || [] === $shortcut) { + if ('' === $shortcut || [] === $shortcut || false === $shortcut) { $shortcut = null; } @@ -96,7 +102,11 @@ public function __construct(string $name, $shortcut = null, ?int $mode = null, s $this->shortcut = $shortcut; $this->mode = $mode; $this->description = $description; + $this->suggestedValues = $suggestedValues; + if ($suggestedValues && !$this->acceptValue()) { + throw new LogicException('Cannot set suggested values if the option does not accept a value.'); + } if ($this->isArray() && !$this->acceptValue()) { throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); } @@ -109,20 +119,16 @@ public function __construct(string $name, $shortcut = null, ?int $mode = null, s /** * Returns the option shortcut. - * - * @return string|null */ - public function getShortcut() + public function getShortcut(): ?string { return $this->shortcut; } /** * Returns the option name. - * - * @return string */ - public function getName() + public function getName(): string { return $this->name; } @@ -132,7 +138,7 @@ public function getName() * * @return bool true if value mode is not self::VALUE_NONE, false otherwise */ - public function acceptValue() + public function acceptValue(): bool { return $this->isValueRequired() || $this->isValueOptional(); } @@ -142,7 +148,7 @@ public function acceptValue() * * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise */ - public function isValueRequired() + public function isValueRequired(): bool { return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); } @@ -152,7 +158,7 @@ public function isValueRequired() * * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise */ - public function isValueOptional() + public function isValueOptional(): bool { return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); } @@ -162,7 +168,7 @@ public function isValueOptional() * * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise */ - public function isArray() + public function isArray(): bool { return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); } @@ -173,10 +179,13 @@ public function isNegatable(): bool } /** - * @param string|bool|int|float|array|null $default + * @return void */ - public function setDefault($default = null) + public function setDefault(string|bool|int|float|array|null $default = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } @@ -194,30 +203,45 @@ public function setDefault($default = null) /** * Returns the default value. - * - * @return string|bool|int|float|array|null */ - public function getDefault() + public function getDefault(): string|bool|int|float|array|null { return $this->default; } /** * Returns the description text. - * - * @return string */ - public function getDescription() + public function getDescription(): string { return $this->description; } + public function hasCompletion(): bool + { + return [] !== $this->suggestedValues; + } + /** - * Checks whether the given option equals this one. + * Adds suggestions to $suggestions for the current completion input. * - * @return bool + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } + + /** + * Checks whether the given option equals this one. */ - public function equals(self $option) + public function equals(self $option): bool { return $option->getName() === $this->getName() && $option->getShortcut() === $this->getShortcut() diff --git a/symfony/console/Input/StreamableInputInterface.php b/symfony/console/Input/StreamableInputInterface.php index d7e462f24..4b95fcb11 100644 --- a/symfony/console/Input/StreamableInputInterface.php +++ b/symfony/console/Input/StreamableInputInterface.php @@ -25,6 +25,8 @@ interface StreamableInputInterface extends InputInterface * This is mainly useful for testing purpose. * * @param resource $stream The input stream + * + * @return void */ public function setStream($stream); diff --git a/symfony/console/Input/StringInput.php b/symfony/console/Input/StringInput.php index 56bb66cbf..82bd21440 100644 --- a/symfony/console/Input/StringInput.php +++ b/symfony/console/Input/StringInput.php @@ -24,6 +24,9 @@ */ class StringInput extends ArgvInput { + /** + * @deprecated since Symfony 6.1 + */ public const REGEX_STRING = '([^\s]+?)(?:\s|(? OutputInterface::VERBOSITY_NORMAL, LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, @@ -40,7 +40,7 @@ class ConsoleLogger extends AbstractLogger LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, ]; - private $formatLevelMap = [ + private array $formatLevelMap = [ LogLevel::EMERGENCY => self::ERROR, LogLevel::ALERT => self::ERROR, LogLevel::CRITICAL => self::ERROR, @@ -50,7 +50,7 @@ class ConsoleLogger extends AbstractLogger LogLevel::INFO => self::INFO, LogLevel::DEBUG => self::INFO, ]; - private $errored = false; + private bool $errored = false; public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = []) { @@ -59,12 +59,7 @@ public function __construct(OutputInterface $output, array $verbosityLevelMap = $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; } - /** - * {@inheritdoc} - * - * @return void - */ - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = []): void { if (!isset($this->verbosityLevelMap[$level])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); @@ -89,10 +84,8 @@ public function log($level, $message, array $context = []) /** * Returns true when any messages have been logged at error levels. - * - * @return bool */ - public function hasErrored() + public function hasErrored(): bool { return $this->errored; } @@ -110,12 +103,12 @@ private function interpolate(string $message, array $context): string $replacements = []; foreach ($context as $key => $val) { - if (null === $val || \is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { + if (null === $val || \is_scalar($val) || $val instanceof \Stringable) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof \DateTimeInterface) { - $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); + $replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339); } elseif (\is_object($val)) { - $replacements["{{$key}}"] = '[object '.\get_class($val).']'; + $replacements["{{$key}}"] = '[object '.$val::class.']'; } else { $replacements["{{$key}}"] = '['.\gettype($val).']'; } diff --git a/symfony/console/Messenger/RunCommandContext.php b/symfony/console/Messenger/RunCommandContext.php new file mode 100644 index 000000000..2ee5415c6 --- /dev/null +++ b/symfony/console/Messenger/RunCommandContext.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +/** + * @author Kevin Bond + */ +final class RunCommandContext +{ + public function __construct( + public readonly RunCommandMessage $message, + public readonly int $exitCode, + public readonly string $output, + ) { + } +} diff --git a/symfony/console/Messenger/RunCommandMessage.php b/symfony/console/Messenger/RunCommandMessage.php new file mode 100644 index 000000000..b530c438c --- /dev/null +++ b/symfony/console/Messenger/RunCommandMessage.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +use Symfony\Component\Console\Exception\RunCommandFailedException; + +/** + * @author Kevin Bond + */ +class RunCommandMessage implements \Stringable +{ + /** + * @param bool $throwOnFailure If the command has a non-zero exit code, throw {@see RunCommandFailedException} + * @param bool $catchExceptions @see Application::setCatchExceptions() + */ + public function __construct( + public readonly string $input, + public readonly bool $throwOnFailure = true, + public readonly bool $catchExceptions = false, + ) { + } + + public function __toString(): string + { + return $this->input; + } +} diff --git a/symfony/console/Messenger/RunCommandMessageHandler.php b/symfony/console/Messenger/RunCommandMessageHandler.php new file mode 100644 index 000000000..14f9c1764 --- /dev/null +++ b/symfony/console/Messenger/RunCommandMessageHandler.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RunCommandFailedException; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\BufferedOutput; + +/** + * @author Kevin Bond + */ +final class RunCommandMessageHandler +{ + public function __construct(private readonly Application $application) + { + } + + public function __invoke(RunCommandMessage $message): RunCommandContext + { + $input = new StringInput($message->input); + $output = new BufferedOutput(); + + $this->application->setCatchExceptions($message->catchExceptions); + + try { + $exitCode = $this->application->run($input, $output); + } catch (\Throwable $e) { + throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch())); + } + + if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) { + throw new RunCommandFailedException(sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch())); + } + + return new RunCommandContext($message, $exitCode, $output->fetch()); + } +} diff --git a/symfony/console/Output/AnsiColorMode.php b/symfony/console/Output/AnsiColorMode.php new file mode 100644 index 000000000..5f9f744fe --- /dev/null +++ b/symfony/console/Output/AnsiColorMode.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Fabien Potencier + * @author Julien Boudry + */ +enum AnsiColorMode +{ + /* + * Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m" + * Must be compatible with all terminals and it's the minimal version supported. + */ + case Ansi4; + + /* + * 8-bit Ansi colors (240 different colors + 16 duplicate color codes, ensuring backward compatibility). + * Output syntax is: "ESC[38;5;${foreGroundColorcode};48;5;${backGroundColorcode}m" + * Should be compatible with most terminals. + */ + case Ansi8; + + /* + * 24-bit Ansi colors (RGB). + * Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m" + * May be compatible with many modern terminals. + */ + case Ansi24; + + /** + * Converts an RGB hexadecimal color to the corresponding Ansi code. + */ + public function convertFromHexToAnsiColorCode(string $hexColor): string + { + $hexColor = str_replace('#', '', $hexColor); + + if (3 === \strlen($hexColor)) { + $hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2]; + } + + if (6 !== \strlen($hexColor)) { + throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor)); + } + + $color = hexdec($hexColor); + + $r = ($color >> 16) & 255; + $g = ($color >> 8) & 255; + $b = $color & 255; + + return match ($this) { + self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b), + self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)), + self::Ansi24 => sprintf('8;2;%d;%d;%d', $r, $g, $b) + }; + } + + private function convertFromRGB(int $r, int $g, int $b): int + { + return match ($this) { + self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b), + self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b), + default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}.") + }; + } + + private function degradeHexColorToAnsi4(int $r, int $g, int $b): int + { + return round($b / 255) << 2 | (round($g / 255) << 1) | round($r / 255); + } + + /** + * Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license). + */ + private function degradeHexColorToAnsi8(int $r, int $g, int $b): int + { + if ($r === $g && $g === $b) { + if ($r < 8) { + return 16; + } + + if ($r > 248) { + return 231; + } + + return (int) round(($r - 8) / 247 * 24) + 232; + } else { + return 16 + + (36 * (int) round($r / 255 * 5)) + + (6 * (int) round($g / 255 * 5)) + + (int) round($b / 255 * 5); + } + } +} diff --git a/symfony/console/Output/BufferedOutput.php b/symfony/console/Output/BufferedOutput.php index d37c6e323..ef5099bfd 100644 --- a/symfony/console/Output/BufferedOutput.php +++ b/symfony/console/Output/BufferedOutput.php @@ -16,14 +16,12 @@ */ class BufferedOutput extends Output { - private $buffer = ''; + private string $buffer = ''; /** * Empties buffer and returns its content. - * - * @return string */ - public function fetch() + public function fetch(): string { $content = $this->buffer; $this->buffer = ''; @@ -32,7 +30,7 @@ public function fetch() } /** - * {@inheritdoc} + * @return void */ protected function doWrite(string $message, bool $newline) { diff --git a/symfony/console/Output/ConsoleOutput.php b/symfony/console/Output/ConsoleOutput.php index 560aeb581..5837e74a3 100644 --- a/symfony/console/Output/ConsoleOutput.php +++ b/symfony/console/Output/ConsoleOutput.php @@ -29,8 +29,8 @@ */ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface { - private $stderr; - private $consoleSectionOutputs = []; + private OutputInterface $stderr; + private array $consoleSectionOutputs = []; /** * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) @@ -65,7 +65,7 @@ public function section(): ConsoleSectionOutput } /** - * {@inheritdoc} + * @return void */ public function setDecorated(bool $decorated) { @@ -74,7 +74,7 @@ public function setDecorated(bool $decorated) } /** - * {@inheritdoc} + * @return void */ public function setFormatter(OutputFormatterInterface $formatter) { @@ -83,7 +83,7 @@ public function setFormatter(OutputFormatterInterface $formatter) } /** - * {@inheritdoc} + * @return void */ public function setVerbosity(int $level) { @@ -91,16 +91,13 @@ public function setVerbosity(int $level) $this->stderr->setVerbosity($level); } - /** - * {@inheritdoc} - */ - public function getErrorOutput() + public function getErrorOutput(): OutputInterface { return $this->stderr; } /** - * {@inheritdoc} + * @return void */ public function setErrorOutput(OutputInterface $error) { @@ -110,10 +107,8 @@ public function setErrorOutput(OutputInterface $error) /** * Returns true if current environment supports writing console output to * STDOUT. - * - * @return bool */ - protected function hasStdoutSupport() + protected function hasStdoutSupport(): bool { return false === $this->isRunningOS400(); } @@ -121,10 +116,8 @@ protected function hasStdoutSupport() /** * Returns true if current environment supports writing console output to * STDERR. - * - * @return bool */ - protected function hasStderrSupport() + protected function hasStderrSupport(): bool { return false === $this->isRunningOS400(); } diff --git a/symfony/console/Output/ConsoleOutputInterface.php b/symfony/console/Output/ConsoleOutputInterface.php index 6b6635f58..9c0049c8f 100644 --- a/symfony/console/Output/ConsoleOutputInterface.php +++ b/symfony/console/Output/ConsoleOutputInterface.php @@ -21,11 +21,12 @@ interface ConsoleOutputInterface extends OutputInterface { /** * Gets the OutputInterface for errors. - * - * @return OutputInterface */ - public function getErrorOutput(); + public function getErrorOutput(): OutputInterface; + /** + * @return void + */ public function setErrorOutput(OutputInterface $error); public function section(): ConsoleSectionOutput; diff --git a/symfony/console/Output/ConsoleSectionOutput.php b/symfony/console/Output/ConsoleSectionOutput.php index 70d70c50b..f2d7933bb 100644 --- a/symfony/console/Output/ConsoleSectionOutput.php +++ b/symfony/console/Output/ConsoleSectionOutput.php @@ -21,10 +21,11 @@ */ class ConsoleSectionOutput extends StreamOutput { - private $content = []; - private $lines = 0; - private $sections; - private $terminal; + private array $content = []; + private int $lines = 0; + private array $sections; + private Terminal $terminal; + private int $maxHeight = 0; /** * @param resource $stream @@ -38,10 +39,29 @@ public function __construct($stream, array &$sections, int $verbosity, bool $dec $this->terminal = new Terminal(); } + /** + * Defines a maximum number of lines for this section. + * + * When more lines are added, the section will automatically scroll to the + * end (i.e. remove the first lines to comply with the max height). + */ + public function setMaxHeight(int $maxHeight): void + { + // when changing max height, clear output of current section and redraw again with the new height + $previousMaxHeight = $this->maxHeight; + $this->maxHeight = $maxHeight; + $existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines); + + parent::doWrite($this->getVisibleContent(), false); + parent::doWrite($existingContent, false); + } + /** * Clears previous output for this section. * * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared + * + * @return void */ public function clear(?int $lines = null) { @@ -50,7 +70,7 @@ public function clear(?int $lines = null) } if ($lines) { - array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content + array_splice($this->content, -$lines); } else { $lines = $this->lines; $this->content = []; @@ -58,15 +78,15 @@ public function clear(?int $lines = null) $this->lines -= $lines; - parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); + parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false); } /** * Overwrites the previous output with a new message. * - * @param array|string $message + * @return void */ - public function overwrite($message) + public function overwrite(string|iterable $message) { $this->clear(); $this->writeln($message); @@ -77,34 +97,110 @@ public function getContent(): string return implode('', $this->content); } + public function getVisibleContent(): string + { + if (0 === $this->maxHeight) { + return $this->getContent(); + } + + return implode('', \array_slice($this->content, -$this->maxHeight)); + } + /** * @internal */ - public function addContent(string $input) + public function addContent(string $input, bool $newline = true): int { - foreach (explode(\PHP_EOL, $input) as $lineContent) { - $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; - $this->content[] = $lineContent; - $this->content[] = \PHP_EOL; + $width = $this->terminal->getWidth(); + $lines = explode(\PHP_EOL, $input); + $linesAdded = 0; + $count = \count($lines) - 1; + foreach ($lines as $i => $lineContent) { + // re-add the line break (that has been removed in the above `explode()` for + // - every line that is not the last line + // - if $newline is required, also add it to the last line + if ($i < $count || $newline) { + $lineContent .= \PHP_EOL; + } + + // skip line if there is no text (or newline for that matter) + if ('' === $lineContent) { + continue; + } + + // For the first line, check if the previous line (last entry of `$this->content`) + // needs to be continued (i.e. does not end with a line break). + if (0 === $i + && (false !== $lastLine = end($this->content)) + && !str_ends_with($lastLine, \PHP_EOL) + ) { + // deduct the line count of the previous line + $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1; + // concatenate previous and new line + $lineContent = $lastLine.$lineContent; + // replace last entry of `$this->content` with the new expanded line + array_splice($this->content, -1, 1, $lineContent); + } else { + // otherwise just add the new content + $this->content[] = $lineContent; + } + + $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1; } + + $this->lines += $linesAdded; + + return $linesAdded; + } + + /** + * @internal + */ + public function addNewLineOfInputSubmit(): void + { + $this->content[] = \PHP_EOL; + ++$this->lines; } /** - * {@inheritdoc} + * @return void */ protected function doWrite(string $message, bool $newline) { + // Simulate newline behavior for consistent output formatting, avoiding extra logic + if (!$newline && str_ends_with($message, \PHP_EOL)) { + $message = substr($message, 0, -\strlen(\PHP_EOL)); + $newline = true; + } + if (!$this->isDecorated()) { parent::doWrite($message, $newline); return; } - $erasedContent = $this->popStreamContentUntilCurrentSection(); + // Check if the previous line (last entry of `$this->content`) needs to be continued + // (i.e. does not end with a line break). In which case, it needs to be erased first. + $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0; + + $linesAdded = $this->addContent($message, $newline); + + if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) { + // on overflow, clear the whole section and redraw again (to remove the first lines) + $linesToClear = $this->maxHeight; + } + + $erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear); - $this->addContent($message); + if ($lineOverflow) { + // redraw existing lines of the section + $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded); + parent::doWrite(implode('', $previousLinesOfSection), false); + } - parent::doWrite($message, true); + // if the last line was removed, re-print its content together with the new content. + // otherwise, just print the new content. + parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true); parent::doWrite($erasedContent, false); } @@ -122,8 +218,13 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr break; } - $numberOfLinesToClear += $section->lines; - $erasedContent[] = $section->getContent(); + $numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines; + if ('' !== $sectionContent = $section->getVisibleContent()) { + if (!str_ends_with($sectionContent, \PHP_EOL)) { + $sectionContent .= \PHP_EOL; + } + $erasedContent[] = $sectionContent; + } } if ($numberOfLinesToClear > 0) { diff --git a/symfony/console/Output/NullOutput.php b/symfony/console/Output/NullOutput.php index 3bbe63ea0..f3aa15b1d 100644 --- a/symfony/console/Output/NullOutput.php +++ b/symfony/console/Output/NullOutput.php @@ -24,104 +24,80 @@ */ class NullOutput implements OutputInterface { - private $formatter; + private NullOutputFormatter $formatter; /** - * {@inheritdoc} + * @return void */ public function setFormatter(OutputFormatterInterface $formatter) { // do nothing } - /** - * {@inheritdoc} - */ - public function getFormatter() + public function getFormatter(): OutputFormatterInterface { - if ($this->formatter) { - return $this->formatter; - } // to comply with the interface we must return a OutputFormatterInterface - return $this->formatter = new NullOutputFormatter(); + return $this->formatter ??= new NullOutputFormatter(); } /** - * {@inheritdoc} + * @return void */ public function setDecorated(bool $decorated) { // do nothing } - /** - * {@inheritdoc} - */ - public function isDecorated() + public function isDecorated(): bool { return false; } /** - * {@inheritdoc} + * @return void */ public function setVerbosity(int $level) { // do nothing } - /** - * {@inheritdoc} - */ - public function getVerbosity() + public function getVerbosity(): int { return self::VERBOSITY_QUIET; } - /** - * {@inheritdoc} - */ - public function isQuiet() + public function isQuiet(): bool { return true; } - /** - * {@inheritdoc} - */ - public function isVerbose() + public function isVerbose(): bool { return false; } - /** - * {@inheritdoc} - */ - public function isVeryVerbose() + public function isVeryVerbose(): bool { return false; } - /** - * {@inheritdoc} - */ - public function isDebug() + public function isDebug(): bool { return false; } /** - * {@inheritdoc} + * @return void */ - public function writeln($messages, int $options = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) { // do nothing } /** - * {@inheritdoc} + * @return void */ - public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) { // do nothing } diff --git a/symfony/console/Output/Output.php b/symfony/console/Output/Output.php index 28c40bb3e..00f481e03 100644 --- a/symfony/console/Output/Output.php +++ b/symfony/console/Output/Output.php @@ -29,8 +29,8 @@ */ abstract class Output implements OutputInterface { - private $verbosity; - private $formatter; + private int $verbosity; + private OutputFormatterInterface $formatter; /** * @param int|null $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) @@ -45,97 +45,76 @@ public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $deco } /** - * {@inheritdoc} + * @return void */ public function setFormatter(OutputFormatterInterface $formatter) { $this->formatter = $formatter; } - /** - * {@inheritdoc} - */ - public function getFormatter() + public function getFormatter(): OutputFormatterInterface { return $this->formatter; } /** - * {@inheritdoc} + * @return void */ public function setDecorated(bool $decorated) { $this->formatter->setDecorated($decorated); } - /** - * {@inheritdoc} - */ - public function isDecorated() + public function isDecorated(): bool { return $this->formatter->isDecorated(); } /** - * {@inheritdoc} + * @return void */ public function setVerbosity(int $level) { $this->verbosity = $level; } - /** - * {@inheritdoc} - */ - public function getVerbosity() + public function getVerbosity(): int { return $this->verbosity; } - /** - * {@inheritdoc} - */ - public function isQuiet() + public function isQuiet(): bool { return self::VERBOSITY_QUIET === $this->verbosity; } - /** - * {@inheritdoc} - */ - public function isVerbose() + public function isVerbose(): bool { return self::VERBOSITY_VERBOSE <= $this->verbosity; } - /** - * {@inheritdoc} - */ - public function isVeryVerbose() + public function isVeryVerbose(): bool { return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; } - /** - * {@inheritdoc} - */ - public function isDebug() + public function isDebug(): bool { return self::VERBOSITY_DEBUG <= $this->verbosity; } /** - * {@inheritdoc} + * @return void */ - public function writeln($messages, int $options = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) { $this->write($messages, true, $options); } /** - * {@inheritdoc} + * @return void */ - public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { $messages = [$messages]; @@ -169,6 +148,8 @@ public function write($messages, bool $newline = false, int $options = self::OUT /** * Writes a message to the output. + * + * @return void */ abstract protected function doWrite(string $message, bool $newline); } diff --git a/symfony/console/Output/OutputInterface.php b/symfony/console/Output/OutputInterface.php index 55caab80b..19a817901 100644 --- a/symfony/console/Output/OutputInterface.php +++ b/symfony/console/Output/OutputInterface.php @@ -33,78 +33,79 @@ interface OutputInterface /** * Writes a message to the output. * - * @param string|iterable $messages The message as an iterable of strings or a single string - * @param bool $newline Whether to add a newline - * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * @param bool $newline Whether to add a newline + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), + * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * + * @return void */ - public function write($messages, bool $newline = false, int $options = 0); + public function write(string|iterable $messages, bool $newline = false, int $options = 0); /** * Writes a message to the output and adds a newline at the end. * - * @param string|iterable $messages The message as an iterable of strings or a single string - * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), + * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * + * @return void */ - public function writeln($messages, int $options = 0); + public function writeln(string|iterable $messages, int $options = 0); /** * Sets the verbosity of the output. + * + * @param self::VERBOSITY_* $level + * + * @return void */ public function setVerbosity(int $level); /** * Gets the current verbosity of the output. * - * @return int + * @return self::VERBOSITY_* */ - public function getVerbosity(); + public function getVerbosity(): int; /** * Returns whether verbosity is quiet (-q). - * - * @return bool */ - public function isQuiet(); + public function isQuiet(): bool; /** * Returns whether verbosity is verbose (-v). - * - * @return bool */ - public function isVerbose(); + public function isVerbose(): bool; /** * Returns whether verbosity is very verbose (-vv). - * - * @return bool */ - public function isVeryVerbose(); + public function isVeryVerbose(): bool; /** * Returns whether verbosity is debug (-vvv). - * - * @return bool */ - public function isDebug(); + public function isDebug(): bool; /** * Sets the decorated flag. + * + * @return void */ public function setDecorated(bool $decorated); /** * Gets the decorated flag. - * - * @return bool */ - public function isDecorated(); + public function isDecorated(): bool; + /** + * @return void + */ public function setFormatter(OutputFormatterInterface $formatter); /** * Returns current output formatter instance. - * - * @return OutputFormatterInterface */ - public function getFormatter(); + public function getFormatter(): OutputFormatterInterface; } diff --git a/symfony/console/Output/StreamOutput.php b/symfony/console/Output/StreamOutput.php index 0ef15cf31..f51d03763 100644 --- a/symfony/console/Output/StreamOutput.php +++ b/symfony/console/Output/StreamOutput.php @@ -29,6 +29,7 @@ */ class StreamOutput extends Output { + /** @var resource */ private $stream; /** @@ -47,9 +48,7 @@ public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?b $this->stream = $stream; - if (null === $decorated) { - $decorated = $this->hasColorSupport(); - } + $decorated ??= $this->hasColorSupport(); parent::__construct($verbosity, $decorated, $formatter); } @@ -64,6 +63,9 @@ public function getStream() return $this->stream; } + /** + * @return void + */ protected function doWrite(string $message, bool $newline) { if ($newline) { @@ -88,57 +90,36 @@ protected function doWrite(string $message, bool $newline) * * @return bool true if the stream supports colorization, false otherwise */ - protected function hasColorSupport() + protected function hasColorSupport(): bool { // Follow https://no-color.org/ - if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) { return false; } - if (!$this->isTty()) { + // Detect msysgit/mingw and assume this is a tty because detection + // does not work correctly, see https://github.com/composer/composer/issues/9690 + if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { return false; } - if (\DIRECTORY_SEPARATOR === '\\' - && \function_exists('sapi_windows_vt100_support') - && @sapi_windows_vt100_support($this->stream) - ) { + if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($this->stream)) { return true; } - return 'Hyper' === getenv('TERM_PROGRAM') + if ('Hyper' === getenv('TERM_PROGRAM') + || false !== getenv('COLORTERM') || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') - || str_starts_with((string) getenv('TERM'), 'xterm'); - } - - /** - * Checks if the stream is a TTY, i.e; whether the output stream is connected to a terminal. - * - * Reference: Composer\Util\Platform::isTty - * https://github.com/composer/composer - */ - private function isTty(): bool - { - // Detect msysgit/mingw and assume this is a tty because detection - // does not work correctly, see https://github.com/composer/composer/issues/9690 - if (\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { + ) { return true; } - // Modern cross-platform function, includes the fstat fallback so if it is present we trust it - if (\function_exists('stream_isatty')) { - return stream_isatty($this->stream); - } - - // Only trusting this if it is positive, otherwise prefer fstat fallback. - if (\function_exists('posix_isatty') && posix_isatty($this->stream)) { - return true; + if ('dumb' === $term = (string) getenv('TERM')) { + return false; } - $stat = @fstat($this->stream); - - // Check if formatted mode is S_IFCHR - return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 + return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); } } diff --git a/symfony/console/Output/TrimmedBufferOutput.php b/symfony/console/Output/TrimmedBufferOutput.php index b08503b3a..23a2be8c3 100644 --- a/symfony/console/Output/TrimmedBufferOutput.php +++ b/symfony/console/Output/TrimmedBufferOutput.php @@ -21,8 +21,8 @@ */ class TrimmedBufferOutput extends Output { - private $maxLength; - private $buffer = ''; + private int $maxLength; + private string $buffer = ''; public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null) { @@ -36,10 +36,8 @@ public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NO /** * Empties buffer and returns its content. - * - * @return string */ - public function fetch() + public function fetch(): string { $content = $this->buffer; $this->buffer = ''; @@ -48,7 +46,7 @@ public function fetch() } /** - * {@inheritdoc} + * @return void */ protected function doWrite(string $message, bool $newline) { diff --git a/symfony/console/Question/ChoiceQuestion.php b/symfony/console/Question/ChoiceQuestion.php index bf1f90487..465f3184f 100644 --- a/symfony/console/Question/ChoiceQuestion.php +++ b/symfony/console/Question/ChoiceQuestion.php @@ -20,17 +20,17 @@ */ class ChoiceQuestion extends Question { - private $choices; - private $multiselect = false; - private $prompt = ' > '; - private $errorMessage = 'Value "%s" is invalid'; + private array $choices; + private bool $multiselect = false; + private string $prompt = ' > '; + private string $errorMessage = 'Value "%s" is invalid'; /** - * @param string $question The question to ask to the user - * @param array $choices The list of available choices - * @param mixed $default The default answer to return + * @param string $question The question to ask to the user + * @param array $choices The list of available choices + * @param string|bool|int|float|null $default The default answer to return */ - public function __construct(string $question, array $choices, $default = null) + public function __construct(string $question, array $choices, string|bool|int|float|null $default = null) { if (!$choices) { throw new \LogicException('Choice question must have at least 1 choice available.'); @@ -45,10 +45,8 @@ public function __construct(string $question, array $choices, $default = null) /** * Returns available choices. - * - * @return array */ - public function getChoices() + public function getChoices(): array { return $this->choices; } @@ -60,7 +58,7 @@ public function getChoices() * * @return $this */ - public function setMultiselect(bool $multiselect) + public function setMultiselect(bool $multiselect): static { $this->multiselect = $multiselect; $this->setValidator($this->getDefaultValidator()); @@ -70,20 +68,16 @@ public function setMultiselect(bool $multiselect) /** * Returns whether the choices are multiselect. - * - * @return bool */ - public function isMultiselect() + public function isMultiselect(): bool { return $this->multiselect; } /** * Gets the prompt for choices. - * - * @return string */ - public function getPrompt() + public function getPrompt(): string { return $this->prompt; } @@ -93,7 +87,7 @@ public function getPrompt() * * @return $this */ - public function setPrompt(string $prompt) + public function setPrompt(string $prompt): static { $this->prompt = $prompt; @@ -107,7 +101,7 @@ public function setPrompt(string $prompt) * * @return $this */ - public function setErrorMessage(string $errorMessage) + public function setErrorMessage(string $errorMessage): static { $this->errorMessage = $errorMessage; $this->setValidator($this->getDefaultValidator()); diff --git a/symfony/console/Question/ConfirmationQuestion.php b/symfony/console/Question/ConfirmationQuestion.php index 4228521b9..40eab2429 100644 --- a/symfony/console/Question/ConfirmationQuestion.php +++ b/symfony/console/Question/ConfirmationQuestion.php @@ -18,7 +18,7 @@ */ class ConfirmationQuestion extends Question { - private $trueAnswerRegex; + private string $trueAnswerRegex; /** * @param string $question The question to ask to the user diff --git a/symfony/console/Question/Question.php b/symfony/console/Question/Question.php index ba5744283..94c688fa8 100644 --- a/symfony/console/Question/Question.php +++ b/symfony/console/Question/Question.php @@ -21,22 +21,22 @@ */ class Question { - private $question; - private $attempts; - private $hidden = false; - private $hiddenFallback = true; - private $autocompleterCallback; - private $validator; - private $default; - private $normalizer; - private $trimmable = true; - private $multiline = false; + private string $question; + private ?int $attempts = null; + private bool $hidden = false; + private bool $hiddenFallback = true; + private ?\Closure $autocompleterCallback = null; + private ?\Closure $validator = null; + private string|int|bool|null|float $default; + private ?\Closure $normalizer = null; + private bool $trimmable = true; + private bool $multiline = false; /** * @param string $question The question to ask to the user * @param string|bool|int|float|null $default The default answer to return if the user enters nothing */ - public function __construct(string $question, $default = null) + public function __construct(string $question, string|bool|int|float|null $default = null) { $this->question = $question; $this->default = $default; @@ -44,20 +44,16 @@ public function __construct(string $question, $default = null) /** * Returns the question. - * - * @return string */ - public function getQuestion() + public function getQuestion(): string { return $this->question; } /** * Returns the default answer. - * - * @return string|bool|int|float|null */ - public function getDefault() + public function getDefault(): string|bool|int|float|null { return $this->default; } @@ -75,7 +71,7 @@ public function isMultiline(): bool * * @return $this */ - public function setMultiline(bool $multiline): self + public function setMultiline(bool $multiline): static { $this->multiline = $multiline; @@ -84,10 +80,8 @@ public function setMultiline(bool $multiline): self /** * Returns whether the user response must be hidden. - * - * @return bool */ - public function isHidden() + public function isHidden(): bool { return $this->hidden; } @@ -99,7 +93,7 @@ public function isHidden() * * @throws LogicException In case the autocompleter is also used */ - public function setHidden(bool $hidden) + public function setHidden(bool $hidden): static { if ($this->autocompleterCallback) { throw new LogicException('A hidden question cannot use the autocompleter.'); @@ -112,10 +106,8 @@ public function setHidden(bool $hidden) /** * In case the response cannot be hidden, whether to fallback on non-hidden question or not. - * - * @return bool */ - public function isHiddenFallback() + public function isHiddenFallback(): bool { return $this->hiddenFallback; } @@ -125,7 +117,7 @@ public function isHiddenFallback() * * @return $this */ - public function setHiddenFallback(bool $fallback) + public function setHiddenFallback(bool $fallback): static { $this->hiddenFallback = $fallback; @@ -134,10 +126,8 @@ public function setHiddenFallback(bool $fallback) /** * Gets values for the autocompleter. - * - * @return iterable|null */ - public function getAutocompleterValues() + public function getAutocompleterValues(): ?iterable { $callback = $this->getAutocompleterCallback(); @@ -151,18 +141,17 @@ public function getAutocompleterValues() * * @throws LogicException */ - public function setAutocompleterValues(?iterable $values) + public function setAutocompleterValues(?iterable $values): static { if (\is_array($values)) { $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values); - $callback = static function () use ($values) { - return $values; - }; + $callback = static fn () => $values; } elseif ($values instanceof \Traversable) { - $valueCache = null; - $callback = static function () use ($values, &$valueCache) { - return $valueCache ?? $valueCache = iterator_to_array($values, false); + $callback = static function () use ($values) { + static $valueCache; + + return $valueCache ??= iterator_to_array($values, false); }; } else { $callback = null; @@ -186,13 +175,16 @@ public function getAutocompleterCallback(): ?callable * * @return $this */ - public function setAutocompleterCallback(?callable $callback = null): self + public function setAutocompleterCallback(?callable $callback = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if ($this->hidden && null !== $callback) { throw new LogicException('A hidden question cannot use the autocompleter.'); } - $this->autocompleterCallback = $callback; + $this->autocompleterCallback = null === $callback ? null : $callback(...); return $this; } @@ -202,19 +194,20 @@ public function setAutocompleterCallback(?callable $callback = null): self * * @return $this */ - public function setValidator(?callable $validator = null) + public function setValidator(?callable $validator = null): static { - $this->validator = $validator; + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->validator = null === $validator ? null : $validator(...); return $this; } /** * Gets the validator for the question. - * - * @return callable|null */ - public function getValidator() + public function getValidator(): ?callable { return $this->validator; } @@ -228,7 +221,7 @@ public function getValidator() * * @throws InvalidArgumentException in case the number of attempts is invalid */ - public function setMaxAttempts(?int $attempts) + public function setMaxAttempts(?int $attempts): static { if (null !== $attempts && $attempts < 1) { throw new InvalidArgumentException('Maximum number of attempts must be a positive value.'); @@ -243,10 +236,8 @@ public function setMaxAttempts(?int $attempts) * Gets the maximum number of attempts. * * Null means an unlimited number of attempts. - * - * @return int|null */ - public function getMaxAttempts() + public function getMaxAttempts(): ?int { return $this->attempts; } @@ -258,9 +249,9 @@ public function getMaxAttempts() * * @return $this */ - public function setNormalizer(callable $normalizer) + public function setNormalizer(callable $normalizer): static { - $this->normalizer = $normalizer; + $this->normalizer = $normalizer(...); return $this; } @@ -269,14 +260,15 @@ public function setNormalizer(callable $normalizer) * Gets the normalizer for the response. * * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. - * - * @return callable|null */ - public function getNormalizer() + public function getNormalizer(): ?callable { return $this->normalizer; } + /** + * @return bool + */ protected function isAssoc(array $array) { return (bool) \count(array_filter(array_keys($array), 'is_string')); @@ -290,7 +282,7 @@ public function isTrimmable(): bool /** * @return $this */ - public function setTrimmable(bool $trimmable): self + public function setTrimmable(bool $trimmable): static { $this->trimmable = $trimmable; diff --git a/symfony/console/Resources/completion.bash b/symfony/console/Resources/completion.bash index 64b87ccf7..64c6a338f 100644 --- a/symfony/console/Resources/completion.bash +++ b/symfony/console/Resources/completion.bash @@ -6,8 +6,18 @@ # https://symfony.com/doc/current/contributing/code/license.html _sf_{{ COMMAND_NAME }}() { + + # Use the default completion for shell redirect operators. + for w in '>' '>>' '&>' '<'; do + if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then + compopt -o filenames + COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}")) + return 0 + fi + done + # Use newline as only separator to allow space in completion values - IFS=$'\n' + local IFS=$'\n' local sf_cmd="${COMP_WORDS[0]}" # for an alias, get the real script behind it @@ -25,7 +35,7 @@ _sf_{{ COMMAND_NAME }}() { local cur prev words cword _get_comp_words_by_ref -n := cur prev words cword - local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-S{{ VERSION }}") + local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}") for w in ${words[@]}; do w=$(printf -- '%b' "$w") # remove quotes from typed values diff --git a/symfony/console/Resources/completion.fish b/symfony/console/Resources/completion.fish new file mode 100644 index 000000000..1c34292ae --- /dev/null +++ b/symfony/console/Resources/completion.fish @@ -0,0 +1,29 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +function _sf_{{ COMMAND_NAME }} + set sf_cmd (commandline -o) + set c (count (commandline -oc)) + + set completecmd "$sf_cmd[1]" "_complete" "--no-interaction" "-sfish" "-a{{ VERSION }}" + + for i in $sf_cmd + if [ $i != "" ] + set completecmd $completecmd "-i$i" + end + end + + set completecmd $completecmd "-c$c" + + set sfcomplete ($completecmd) + + for i in $sfcomplete + echo $i + end +end + +complete -c '{{ COMMAND_NAME }}' -a '(_sf_{{ COMMAND_NAME }})' -f diff --git a/symfony/console/Resources/completion.zsh b/symfony/console/Resources/completion.zsh new file mode 100644 index 000000000..ff76fe5fa --- /dev/null +++ b/symfony/console/Resources/completion.zsh @@ -0,0 +1,82 @@ +#compdef {{ COMMAND_NAME }} + +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +# +# zsh completions for {{ COMMAND_NAME }} +# +# References: +# - https://github.com/spf13/cobra/blob/master/zsh_completions.go +# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash +# +_sf_{{ COMMAND_NAME }}() { + local lastParam flagPrefix requestComp out comp + local -a completions + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") lastParam=${words[-1]} + + # For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a{{ VERSION }} -c$((CURRENT-1))" i="" + for w in ${words[@]}; do + w=$(printf -- '%b' "$w") + # remove quotes from typed values + quote="${w:0:1}" + if [ "$quote" = \' ]; then + w="${w%\'}" + w="${w#\'}" + elif [ "$quote" = \" ]; then + w="${w%\"}" + w="${w#\"}" + fi + # empty values are ignored + if [ ! -z "$w" ]; then + i="${i}-i${w} " + fi + done + + # Ensure at least 1 input + if [ "${i}" = "" ]; then + requestComp="${requestComp} -i\" \"" + else + requestComp="${requestComp} ${i}" + fi + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + local tab=$(printf '\t') + comp=${comp//$tab/:} + completions+=${comp} + fi + done < <(printf "%s\n" "${out[@]}") + + # Let inbuilt _describe handle completions + eval _describe "completions" completions $flagPrefix + return $? +} + +compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} diff --git a/symfony/console/SignalRegistry/SignalMap.php b/symfony/console/SignalRegistry/SignalMap.php new file mode 100644 index 000000000..de419bda7 --- /dev/null +++ b/symfony/console/SignalRegistry/SignalMap.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\SignalRegistry; + +/** + * @author Grégoire Pineau + */ +class SignalMap +{ + private static array $map; + + public static function getSignalName(int $signal): ?string + { + if (!\extension_loaded('pcntl')) { + return null; + } + + if (!isset(self::$map)) { + $r = new \ReflectionExtension('pcntl'); + $c = $r->getConstants(); + $map = array_filter($c, fn ($k) => str_starts_with($k, 'SIG') && !str_starts_with($k, 'SIG_'), \ARRAY_FILTER_USE_KEY); + self::$map = array_flip($map); + } + + return self::$map[$signal] ?? null; + } +} diff --git a/symfony/console/SignalRegistry/SignalRegistry.php b/symfony/console/SignalRegistry/SignalRegistry.php index 6bee24a42..ef2e5f04e 100644 --- a/symfony/console/SignalRegistry/SignalRegistry.php +++ b/symfony/console/SignalRegistry/SignalRegistry.php @@ -13,7 +13,7 @@ final class SignalRegistry { - private $signalHandlers = []; + private array $signalHandlers = []; public function __construct() { @@ -34,20 +34,12 @@ public function register(int $signal, callable $signalHandler): void $this->signalHandlers[$signal][] = $signalHandler; - pcntl_signal($signal, [$this, 'handle']); + pcntl_signal($signal, $this->handle(...)); } public static function isSupported(): bool { - if (!\function_exists('pcntl_signal')) { - return false; - } - - if (\in_array('pcntl_signal', explode(',', \ini_get('disable_functions')))) { - return false; - } - - return true; + return \function_exists('pcntl_signal'); } /** diff --git a/symfony/console/SingleCommandApplication.php b/symfony/console/SingleCommandApplication.php index 774e5d8c4..ff1c17247 100644 --- a/symfony/console/SingleCommandApplication.php +++ b/symfony/console/SingleCommandApplication.php @@ -20,14 +20,14 @@ */ class SingleCommandApplication extends Command { - private $version = 'UNKNOWN'; - private $autoExit = true; - private $running = false; + private string $version = 'UNKNOWN'; + private bool $autoExit = true; + private bool $running = false; /** * @return $this */ - public function setVersion(string $version): self + public function setVersion(string $version): static { $this->version = $version; @@ -39,7 +39,7 @@ public function setVersion(string $version): self * * @return $this */ - public function setAutoExit(bool $autoExit): self + public function setAutoExit(bool $autoExit): static { $this->autoExit = $autoExit; diff --git a/symfony/console/Style/OutputStyle.php b/symfony/console/Style/OutputStyle.php index 67a98ff07..ddfa8decc 100644 --- a/symfony/console/Style/OutputStyle.php +++ b/symfony/console/Style/OutputStyle.php @@ -23,7 +23,7 @@ */ abstract class OutputStyle implements OutputInterface, StyleInterface { - private $output; + private OutputInterface $output; public function __construct(OutputInterface $output) { @@ -31,117 +31,96 @@ public function __construct(OutputInterface $output) } /** - * {@inheritdoc} + * @return void */ public function newLine(int $count = 1) { $this->output->write(str_repeat(\PHP_EOL, $count)); } - /** - * @return ProgressBar - */ - public function createProgressBar(int $max = 0) + public function createProgressBar(int $max = 0): ProgressBar { return new ProgressBar($this->output, $max); } /** - * {@inheritdoc} + * @return void */ - public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) { $this->output->write($messages, $newline, $type); } /** - * {@inheritdoc} + * @return void */ - public function writeln($messages, int $type = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) { $this->output->writeln($messages, $type); } /** - * {@inheritdoc} + * @return void */ public function setVerbosity(int $level) { $this->output->setVerbosity($level); } - /** - * {@inheritdoc} - */ - public function getVerbosity() + public function getVerbosity(): int { return $this->output->getVerbosity(); } /** - * {@inheritdoc} + * @return void */ public function setDecorated(bool $decorated) { $this->output->setDecorated($decorated); } - /** - * {@inheritdoc} - */ - public function isDecorated() + public function isDecorated(): bool { return $this->output->isDecorated(); } /** - * {@inheritdoc} + * @return void */ public function setFormatter(OutputFormatterInterface $formatter) { $this->output->setFormatter($formatter); } - /** - * {@inheritdoc} - */ - public function getFormatter() + public function getFormatter(): OutputFormatterInterface { return $this->output->getFormatter(); } - /** - * {@inheritdoc} - */ - public function isQuiet() + public function isQuiet(): bool { return $this->output->isQuiet(); } - /** - * {@inheritdoc} - */ - public function isVerbose() + public function isVerbose(): bool { return $this->output->isVerbose(); } - /** - * {@inheritdoc} - */ - public function isVeryVerbose() + public function isVeryVerbose(): bool { return $this->output->isVeryVerbose(); } - /** - * {@inheritdoc} - */ - public function isDebug() + public function isDebug(): bool { return $this->output->isDebug(); } + /** + * @return OutputInterface + */ protected function getErrorOutput() { if (!$this->output instanceof ConsoleOutputInterface) { diff --git a/symfony/console/Style/StyleInterface.php b/symfony/console/Style/StyleInterface.php index 9f25a43f6..6bced158a 100644 --- a/symfony/console/Style/StyleInterface.php +++ b/symfony/console/Style/StyleInterface.php @@ -20,113 +20,119 @@ interface StyleInterface { /** * Formats a command title. + * + * @return void */ public function title(string $message); /** * Formats a section title. + * + * @return void */ public function section(string $message); /** * Formats a list. + * + * @return void */ public function listing(array $elements); /** * Formats informational text. * - * @param string|array $message + * @return void */ - public function text($message); + public function text(string|array $message); /** * Formats a success result bar. * - * @param string|array $message + * @return void */ - public function success($message); + public function success(string|array $message); /** * Formats an error result bar. * - * @param string|array $message + * @return void */ - public function error($message); + public function error(string|array $message); /** * Formats an warning result bar. * - * @param string|array $message + * @return void */ - public function warning($message); + public function warning(string|array $message); /** * Formats a note admonition. * - * @param string|array $message + * @return void */ - public function note($message); + public function note(string|array $message); /** * Formats a caution admonition. * - * @param string|array $message + * @return void */ - public function caution($message); + public function caution(string|array $message); /** * Formats a table. + * + * @return void */ public function table(array $headers, array $rows); /** * Asks a question. - * - * @return mixed */ - public function ask(string $question, ?string $default = null, ?callable $validator = null); + public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed; /** * Asks a question with the user input hidden. - * - * @return mixed */ - public function askHidden(string $question, ?callable $validator = null); + public function askHidden(string $question, ?callable $validator = null): mixed; /** * Asks for confirmation. - * - * @return bool */ - public function confirm(string $question, bool $default = true); + public function confirm(string $question, bool $default = true): bool; /** * Asks a choice question. - * - * @param string|int|null $default - * - * @return mixed */ - public function choice(string $question, array $choices, $default = null); + public function choice(string $question, array $choices, mixed $default = null): mixed; /** * Add newline(s). + * + * @return void */ public function newLine(int $count = 1); /** * Starts the progress output. + * + * @return void */ public function progressStart(int $max = 0); /** * Advances the progress output X steps. + * + * @return void */ public function progressAdvance(int $step = 1); /** * Finishes the progress output. + * + * @return void */ public function progressFinish(); } diff --git a/symfony/console/Style/SymfonyStyle.php b/symfony/console/Style/SymfonyStyle.php index 00edf3882..03bda8784 100644 --- a/symfony/console/Style/SymfonyStyle.php +++ b/symfony/console/Style/SymfonyStyle.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\OutputWrapper; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Helper\Table; @@ -22,6 +23,7 @@ use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\TrimmedBufferOutput; use Symfony\Component\Console\Question\ChoiceQuestion; @@ -38,12 +40,12 @@ class SymfonyStyle extends OutputStyle { public const MAX_LINE_LENGTH = 120; - private $input; - private $output; - private $questionHelper; - private $progressBar; - private $lineLength; - private $bufferedOutput; + private InputInterface $input; + private OutputInterface $output; + private SymfonyQuestionHelper $questionHelper; + private ProgressBar $progressBar; + private int $lineLength; + private TrimmedBufferOutput $bufferedOutput; public function __construct(InputInterface $input, OutputInterface $output) { @@ -59,9 +61,9 @@ public function __construct(InputInterface $input, OutputInterface $output) /** * Formats a message as a block of text. * - * @param string|array $messages The message to write in the block + * @return void */ - public function block($messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true) + public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true) { $messages = \is_array($messages) ? array_values($messages) : [$messages]; @@ -71,7 +73,7 @@ public function block($messages, ?string $type = null, ?string $style = null, st } /** - * {@inheritdoc} + * @return void */ public function title(string $message) { @@ -84,7 +86,7 @@ public function title(string $message) } /** - * {@inheritdoc} + * @return void */ public function section(string $message) { @@ -97,23 +99,21 @@ public function section(string $message) } /** - * {@inheritdoc} + * @return void */ public function listing(array $elements) { $this->autoPrependText(); - $elements = array_map(function ($element) { - return sprintf(' * %s', $element); - }, $elements); + $elements = array_map(fn ($element) => sprintf(' * %s', $element), $elements); $this->writeln($elements); $this->newLine(); } /** - * {@inheritdoc} + * @return void */ - public function text($message) + public function text(string|array $message) { $this->autoPrependText(); @@ -126,41 +126,41 @@ public function text($message) /** * Formats a command comment. * - * @param string|array $message + * @return void */ - public function comment($message) + public function comment(string|array $message) { $this->block($message, null, null, ' // ', false, false); } /** - * {@inheritdoc} + * @return void */ - public function success($message) + public function success(string|array $message) { $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); } /** - * {@inheritdoc} + * @return void */ - public function error($message) + public function error(string|array $message) { $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); } /** - * {@inheritdoc} + * @return void */ - public function warning($message) + public function warning(string|array $message) { $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true); } /** - * {@inheritdoc} + * @return void */ - public function note($message) + public function note(string|array $message) { $this->block($message, 'NOTE', 'fg=yellow', ' ! '); } @@ -168,23 +168,23 @@ public function note($message) /** * Formats an info message. * - * @param string|array $message + * @return void */ - public function info($message) + public function info(string|array $message) { $this->block($message, 'INFO', 'fg=green', ' ', true); } /** - * {@inheritdoc} + * @return void */ - public function caution($message) + public function caution(string|array $message) { $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); } /** - * {@inheritdoc} + * @return void */ public function table(array $headers, array $rows) { @@ -199,6 +199,8 @@ public function table(array $headers, array $rows) /** * Formats a horizontal table. + * + * @return void */ public function horizontalTable(array $headers, array $rows) { @@ -220,9 +222,9 @@ public function horizontalTable(array $headers, array $rows) * * ['key' => 'value'] * * new TableSeparator() * - * @param string|array|TableSeparator ...$list + * @return void */ - public function definitionList(...$list) + public function definitionList(string|array|TableSeparator ...$list) { $headers = []; $row = []; @@ -247,10 +249,7 @@ public function definitionList(...$list) $this->horizontalTable($headers, [$row]); } - /** - * {@inheritdoc} - */ - public function ask(string $question, ?string $default = null, ?callable $validator = null) + public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed { $question = new Question($question, $default); $question->setValidator($validator); @@ -258,10 +257,7 @@ public function ask(string $question, ?string $default = null, ?callable $valida return $this->askQuestion($question); } - /** - * {@inheritdoc} - */ - public function askHidden(string $question, ?callable $validator = null) + public function askHidden(string $question, ?callable $validator = null): mixed { $question = new Question($question); @@ -271,29 +267,26 @@ public function askHidden(string $question, ?callable $validator = null) return $this->askQuestion($question); } - /** - * {@inheritdoc} - */ - public function confirm(string $question, bool $default = true) + public function confirm(string $question, bool $default = true): bool { return $this->askQuestion(new ConfirmationQuestion($question, $default)); } - /** - * {@inheritdoc} - */ - public function choice(string $question, array $choices, $default = null) + public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed { if (null !== $default) { $values = array_flip($choices); $default = $values[$default] ?? $default; } - return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); + $questionChoice = new ChoiceQuestion($question, $choices, $default); + $questionChoice->setMultiselect($multiSelect); + + return $this->askQuestion($questionChoice); } /** - * {@inheritdoc} + * @return void */ public function progressStart(int $max = 0) { @@ -302,7 +295,7 @@ public function progressStart(int $max = 0) } /** - * {@inheritdoc} + * @return void */ public function progressAdvance(int $step = 1) { @@ -310,19 +303,16 @@ public function progressAdvance(int $step = 1) } /** - * {@inheritdoc} + * @return void */ public function progressFinish() { $this->getProgressBar()->finish(); $this->newLine(2); - $this->progressBar = null; + unset($this->progressBar); } - /** - * {@inheritdoc} - */ - public function createProgressBar(int $max = 0) + public function createProgressBar(int $max = 0): ProgressBar { $progressBar = parent::createProgressBar($max); @@ -337,6 +327,14 @@ public function createProgressBar(int $max = 0) /** * @see ProgressBar::iterate() + * + * @template TKey + * @template TValue + * + * @param iterable $iterable + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable + * + * @return iterable */ public function progressIterate(iterable $iterable, ?int $max = null): iterable { @@ -345,22 +343,22 @@ public function progressIterate(iterable $iterable, ?int $max = null): iterable $this->newLine(2); } - /** - * @return mixed - */ - public function askQuestion(Question $question) + public function askQuestion(Question $question): mixed { if ($this->input->isInteractive()) { $this->autoPrependBlock(); } - if (!$this->questionHelper) { - $this->questionHelper = new SymfonyQuestionHelper(); - } + $this->questionHelper ??= new SymfonyQuestionHelper(); $answer = $this->questionHelper->ask($this->input, $this, $question); if ($this->input->isInteractive()) { + if ($this->output instanceof ConsoleSectionOutput) { + // add the new line of the `return` to submit the input to ConsoleSectionOutput, because ConsoleSectionOutput is holding all it's lines. + // this is relevant when a `ConsoleSectionOutput::clear` is called. + $this->output->addNewLineOfInputSubmit(); + } $this->newLine(); $this->bufferedOutput->write("\n"); } @@ -369,9 +367,9 @@ public function askQuestion(Question $question) } /** - * {@inheritdoc} + * @return void */ - public function writeln($messages, int $type = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { $messages = [$messages]; @@ -384,9 +382,9 @@ public function writeln($messages, int $type = self::OUTPUT_NORMAL) } /** - * {@inheritdoc} + * @return void */ - public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { $messages = [$messages]; @@ -399,7 +397,7 @@ public function write($messages, bool $newline = false, int $type = self::OUTPUT } /** - * {@inheritdoc} + * @return void */ public function newLine(int $count = 1) { @@ -409,10 +407,8 @@ public function newLine(int $count = 1) /** * Returns a new instance which makes use of stderr if available. - * - * @return self */ - public function getErrorStyle() + public function getErrorStyle(): self { return new self($this->input, $this->getErrorOutput()); } @@ -428,11 +424,8 @@ public function createTable(): Table private function getProgressBar(): ProgressBar { - if (!$this->progressBar) { - throw new RuntimeException('The ProgressBar is not started.'); - } - - return $this->progressBar; + return $this->progressBar + ?? throw new RuntimeException('The ProgressBar is not started.'); } private function autoPrependBlock(): void @@ -452,7 +445,7 @@ private function autoPrependText(): void { $fetched = $this->bufferedOutput->fetch(); // Prepend new line if last char isn't EOL: - if (!str_ends_with($fetched, "\n")) { + if ($fetched && !str_ends_with($fetched, "\n")) { $this->newLine(); } } @@ -471,22 +464,25 @@ private function createBlock(iterable $messages, ?string $type = null, ?string $ if (null !== $type) { $type = sprintf('[%s] ', $type); - $indentLength = \strlen($type); + $indentLength = Helper::width($type); $lineIndentation = str_repeat(' ', $indentLength); } // wrap and add newlines for each element + $outputWrapper = new OutputWrapper(); foreach ($messages as $key => $message) { if ($escape) { $message = OutputFormatter::escape($message); } - $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message)); - $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); - $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); - foreach ($messageLines as $messageLine) { - $lines[] = $messageLine; - } + $lines = array_merge( + $lines, + explode(\PHP_EOL, $outputWrapper->wrap( + $message, + $this->lineLength - $prefixLength - $indentLength, + \PHP_EOL + )) + ); if (\count($messages) > 1 && $key < \count($messages) - 1) { $lines[] = ''; diff --git a/symfony/console/Terminal.php b/symfony/console/Terminal.php index b91e8afc5..9ac17f601 100644 --- a/symfony/console/Terminal.php +++ b/symfony/console/Terminal.php @@ -11,18 +11,79 @@ namespace Symfony\Component\Console; +use Symfony\Component\Console\Output\AnsiColorMode; + class Terminal { - private static $width; - private static $height; - private static $stty; + public const DEFAULT_COLOR_MODE = AnsiColorMode::Ansi4; + + private static ?AnsiColorMode $colorMode = null; + private static ?int $width = null; + private static ?int $height = null; + private static ?bool $stty = null; + + /** + * About Ansi color types: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + * For more information about true color support with terminals https://github.com/termstandard/colors/. + */ + public static function getColorMode(): AnsiColorMode + { + // Use Cache from previous run (or user forced mode) + if (null !== self::$colorMode) { + return self::$colorMode; + } + + // Try with $COLORTERM first + if (\is_string($colorterm = getenv('COLORTERM'))) { + $colorterm = strtolower($colorterm); + + if (str_contains($colorterm, 'truecolor')) { + self::setColorMode(AnsiColorMode::Ansi24); + + return self::$colorMode; + } + + if (str_contains($colorterm, '256color')) { + self::setColorMode(AnsiColorMode::Ansi8); + + return self::$colorMode; + } + } + + // Try with $TERM + if (\is_string($term = getenv('TERM'))) { + $term = strtolower($term); + + if (str_contains($term, 'truecolor')) { + self::setColorMode(AnsiColorMode::Ansi24); + + return self::$colorMode; + } + + if (str_contains($term, '256color')) { + self::setColorMode(AnsiColorMode::Ansi8); + + return self::$colorMode; + } + } + + self::setColorMode(self::DEFAULT_COLOR_MODE); + + return self::$colorMode; + } + + /** + * Force a terminal color mode rendering. + */ + public static function setColorMode(?AnsiColorMode $colorMode): void + { + self::$colorMode = $colorMode; + } /** * Gets the terminal width. - * - * @return int */ - public function getWidth() + public function getWidth(): int { $width = getenv('COLUMNS'); if (false !== $width) { @@ -38,10 +99,8 @@ public function getWidth() /** * Gets the terminal height. - * - * @return int */ - public function getHeight() + public function getHeight(): int { $height = getenv('LINES'); if (false !== $height) { @@ -72,7 +131,7 @@ public static function hasSttyAvailable(): bool return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } - private static function initDimensions() + private static function initDimensions(): void { if ('\\' === \DIRECTORY_SEPARATOR) { $ansicon = getenv('ANSICON'); @@ -106,14 +165,14 @@ private static function hasVt100Support(): bool /** * Initializes dimensions using the output of an stty columns line. */ - private static function initDimensionsUsingStty() + private static function initDimensionsUsingStty(): void { if ($sttyString = self::getSttyColumns()) { - if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + if (preg_match('/rows.(\d+);.columns.(\d+);/is', $sttyString, $matches)) { // extract [w, h] from "rows h; columns w;" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; - } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/is', $sttyString, $matches)) { // extract [w, h] from "; h rows; w columns" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; @@ -142,10 +201,10 @@ private static function getConsoleMode(): ?array */ private static function getSttyColumns(): ?string { - return self::readFromProcess('stty -a | grep columns'); + return self::readFromProcess(['stty', '-a']); } - private static function readFromProcess(string $command): ?string + private static function readFromProcess(string|array $command): ?string { if (!\function_exists('proc_open')) { return null; @@ -158,8 +217,7 @@ private static function readFromProcess(string $command): ?string $cp = \function_exists('sapi_windows_cp_set') ? sapi_windows_cp_get() : 0; - $process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); - if (!\is_resource($process)) { + if (!$process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true])) { return null; } diff --git a/symfony/console/Tester/ApplicationTester.php b/symfony/console/Tester/ApplicationTester.php index 3a262e81c..58aee54d6 100644 --- a/symfony/console/Tester/ApplicationTester.php +++ b/symfony/console/Tester/ApplicationTester.php @@ -28,7 +28,7 @@ class ApplicationTester { use TesterTrait; - private $application; + private Application $application; public function __construct(Application $application) { @@ -47,7 +47,7 @@ public function __construct(Application $application) * * @return int The command exit code */ - public function run(array $input, array $options = []) + public function run(array $input, array $options = []): int { $prevShellVerbosity = getenv('SHELL_VERBOSITY'); diff --git a/symfony/console/Tester/CommandCompletionTester.php b/symfony/console/Tester/CommandCompletionTester.php index ade732752..a90fe52ef 100644 --- a/symfony/console/Tester/CommandCompletionTester.php +++ b/symfony/console/Tester/CommandCompletionTester.php @@ -22,7 +22,7 @@ */ class CommandCompletionTester { - private $command; + private Command $command; public function __construct(Command $command) { diff --git a/symfony/console/Tester/CommandTester.php b/symfony/console/Tester/CommandTester.php index 6c15c25fb..2ff813b7d 100644 --- a/symfony/console/Tester/CommandTester.php +++ b/symfony/console/Tester/CommandTester.php @@ -24,7 +24,7 @@ class CommandTester { use TesterTrait; - private $command; + private Command $command; public function __construct(Command $command) { @@ -46,7 +46,7 @@ public function __construct(Command $command) * * @return int The command exit code */ - public function execute(array $input, array $options = []) + public function execute(array $input, array $options = []): int { // set the command name automatically if the application requires // this argument and no command name was passed diff --git a/symfony/console/Tester/Constraint/CommandIsSuccessful.php b/symfony/console/Tester/Constraint/CommandIsSuccessful.php index a47324237..09c6194b9 100644 --- a/symfony/console/Tester/Constraint/CommandIsSuccessful.php +++ b/symfony/console/Tester/Constraint/CommandIsSuccessful.php @@ -16,33 +16,21 @@ final class CommandIsSuccessful extends Constraint { - /** - * {@inheritdoc} - */ public function toString(): string { return 'is successful'; } - /** - * {@inheritdoc} - */ protected function matches($other): bool { return Command::SUCCESS === $other; } - /** - * {@inheritdoc} - */ protected function failureDescription($other): string { return 'the command '.$this->toString(); } - /** - * {@inheritdoc} - */ protected function additionalFailureDescription($other): string { $mapping = [ diff --git a/symfony/console/Tester/TesterTrait.php b/symfony/console/Tester/TesterTrait.php index f454bbf9d..1ab7a70aa 100644 --- a/symfony/console/Tester/TesterTrait.php +++ b/symfony/console/Tester/TesterTrait.php @@ -23,25 +23,20 @@ */ trait TesterTrait { - /** @var StreamOutput */ - private $output; - private $inputs = []; - private $captureStreamsIndependently = false; - /** @var InputInterface */ - private $input; - /** @var int */ - private $statusCode; + private StreamOutput $output; + private array $inputs = []; + private bool $captureStreamsIndependently = false; + private InputInterface $input; + private int $statusCode; /** * Gets the display returned by the last execution of the command or application. * - * @return string - * * @throws \RuntimeException If it's called before the execute method */ - public function getDisplay(bool $normalize = false) + public function getDisplay(bool $normalize = false): string { - if (null === $this->output) { + if (!isset($this->output)) { throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?'); } @@ -60,10 +55,8 @@ public function getDisplay(bool $normalize = false) * Gets the output written to STDERR by the application. * * @param bool $normalize Whether to normalize end of lines to \n or not - * - * @return string */ - public function getErrorOutput(bool $normalize = false) + public function getErrorOutput(bool $normalize = false): string { if (!$this->captureStreamsIndependently) { throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.'); @@ -82,20 +75,16 @@ public function getErrorOutput(bool $normalize = false) /** * Gets the input instance used by the last execution of the command or application. - * - * @return InputInterface */ - public function getInput() + public function getInput(): InputInterface { return $this->input; } /** * Gets the output instance used by the last execution of the command or application. - * - * @return OutputInterface */ - public function getOutput() + public function getOutput(): OutputInterface { return $this->output; } @@ -103,17 +92,11 @@ public function getOutput() /** * Gets the status code returned by the last execution of the command or application. * - * @return int - * * @throws \RuntimeException If it's called before the execute method */ - public function getStatusCode() + public function getStatusCode(): int { - if (null === $this->statusCode) { - throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?'); - } - - return $this->statusCode; + return $this->statusCode ?? throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?'); } public function assertCommandIsSuccessful(string $message = ''): void @@ -129,7 +112,7 @@ public function assertCommandIsSuccessful(string $message = ''): void * * @return $this */ - public function setInputs(array $inputs) + public function setInputs(array $inputs): static { $this->inputs = $inputs; @@ -145,9 +128,9 @@ public function setInputs(array $inputs) * * verbosity: Sets the output verbosity flag * * capture_stderr_separately: Make output of stdOut and stdErr separately available */ - private function initOutput(array $options) + private function initOutput(array $options): void { - $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; + $this->captureStreamsIndependently = $options['capture_stderr_separately'] ?? false; if (!$this->captureStreamsIndependently) { $this->output = new StreamOutput(fopen('php://memory', 'w', false)); if (isset($options['decorated'])) { @@ -169,12 +152,10 @@ private function initOutput(array $options) $reflectedOutput = new \ReflectionObject($this->output); $strErrProperty = $reflectedOutput->getProperty('stderr'); - $strErrProperty->setAccessible(true); $strErrProperty->setValue($this->output, $errorOutput); $reflectedParent = $reflectedOutput->getParentClass(); $streamProperty = $reflectedParent->getProperty('stream'); - $streamProperty->setAccessible(true); $streamProperty->setValue($this->output, fopen('php://memory', 'w', false)); } } diff --git a/symfony/event-dispatcher-contracts/Event.php b/symfony/event-dispatcher-contracts/Event.php index 384a650b8..2e7f99890 100644 --- a/symfony/event-dispatcher-contracts/Event.php +++ b/symfony/event-dispatcher-contracts/Event.php @@ -32,9 +32,6 @@ class Event implements StoppableEventInterface { private bool $propagationStopped = false; - /** - * {@inheritdoc} - */ public function isPropagationStopped(): bool { return $this->propagationStopped; diff --git a/symfony/event-dispatcher-contracts/EventDispatcherInterface.php b/symfony/event-dispatcher-contracts/EventDispatcherInterface.php index 351dc5131..2d7840d32 100644 --- a/symfony/event-dispatcher-contracts/EventDispatcherInterface.php +++ b/symfony/event-dispatcher-contracts/EventDispatcherInterface.php @@ -21,11 +21,13 @@ interface EventDispatcherInterface extends PsrEventDispatcherInterface /** * Dispatches an event to all registered listeners. * - * @param object $event The event to pass to the event handlers/listeners + * @template T of object + * + * @param T $event The event to pass to the event handlers/listeners * @param string|null $eventName The name of the event to dispatch. If not supplied, * the class of $event should be used instead. * - * @return object The passed $event MUST be returned + * @return T The passed $event MUST be returned */ - public function dispatch(object $event, string $eventName = null): object; + public function dispatch(object $event, ?string $eventName = null): object; } diff --git a/symfony/event-dispatcher-contracts/LICENSE b/symfony/event-dispatcher-contracts/LICENSE index 74cdc2dbf..7536caeae 100644 --- a/symfony/event-dispatcher-contracts/LICENSE +++ b/symfony/event-dispatcher-contracts/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2022 Fabien Potencier +Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php b/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php index acfbf619c..5ba83dad4 100644 --- a/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php +++ b/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php @@ -13,6 +13,7 @@ use Psr\EventDispatcher\StoppableEventInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -33,35 +34,33 @@ class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterfa protected $stopwatch; /** - * @var \SplObjectStorage + * @var \SplObjectStorage|null */ - private $callStack; - private $dispatcher; - private $wrappedListeners; - private $orphanedEvents; - private $requestStack; - private $currentRequestHash = ''; - - public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null, RequestStack $requestStack = null) + private ?\SplObjectStorage $callStack = null; + private EventDispatcherInterface $dispatcher; + private array $wrappedListeners = []; + private array $orphanedEvents = []; + private ?RequestStack $requestStack; + private string $currentRequestHash = ''; + + public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, ?LoggerInterface $logger = null, ?RequestStack $requestStack = null) { $this->dispatcher = $dispatcher; $this->stopwatch = $stopwatch; $this->logger = $logger; - $this->wrappedListeners = []; - $this->orphanedEvents = []; $this->requestStack = $requestStack; } /** - * {@inheritdoc} + * @return void */ - public function addListener(string $eventName, $listener, int $priority = 0) + public function addListener(string $eventName, callable|array $listener, int $priority = 0) { $this->dispatcher->addListener($eventName, $listener, $priority); } /** - * {@inheritdoc} + * @return void */ public function addSubscriber(EventSubscriberInterface $subscriber) { @@ -69,9 +68,9 @@ public function addSubscriber(EventSubscriberInterface $subscriber) } /** - * {@inheritdoc} + * @return void */ - public function removeListener(string $eventName, $listener) + public function removeListener(string $eventName, callable|array $listener) { if (isset($this->wrappedListeners[$eventName])) { foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { @@ -83,29 +82,23 @@ public function removeListener(string $eventName, $listener) } } - return $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->removeListener($eventName, $listener); } /** - * {@inheritdoc} + * @return void */ public function removeSubscriber(EventSubscriberInterface $subscriber) { - return $this->dispatcher->removeSubscriber($subscriber); + $this->dispatcher->removeSubscriber($subscriber); } - /** - * {@inheritdoc} - */ - public function getListeners(string $eventName = null) + public function getListeners(?string $eventName = null): array { return $this->dispatcher->getListeners($eventName); } - /** - * {@inheritdoc} - */ - public function getListenerPriority(string $eventName, $listener) + public function getListenerPriority(string $eventName, callable|array $listener): ?int { // we might have wrapped listeners for the event (if called while dispatching) // in that case get the priority by wrapper @@ -120,24 +113,16 @@ public function getListenerPriority(string $eventName, $listener) return $this->dispatcher->getListenerPriority($eventName, $listener); } - /** - * {@inheritdoc} - */ - public function hasListeners(string $eventName = null) + public function hasListeners(?string $eventName = null): bool { return $this->dispatcher->hasListeners($eventName); } - /** - * {@inheritdoc} - */ - public function dispatch(object $event, string $eventName = null): object + public function dispatch(object $event, ?string $eventName = null): object { - $eventName = $eventName ?? \get_class($event); + $eventName ??= $event::class; - if (null === $this->callStack) { - $this->callStack = new \SplObjectStorage(); - } + $this->callStack ??= new \SplObjectStorage(); $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; @@ -168,10 +153,7 @@ public function dispatch(object $event, string $eventName = null): object return $event; } - /** - * @return array - */ - public function getCalledListeners(Request $request = null) + public function getCalledListeners(?Request $request = null): array { if (null === $this->callStack) { return []; @@ -189,17 +171,12 @@ public function getCalledListeners(Request $request = null) return $called; } - /** - * @return array - */ - public function getNotCalledListeners(Request $request = null) + public function getNotCalledListeners(?Request $request = null): array { try { - $allListeners = $this->getListeners(); + $allListeners = $this->dispatcher instanceof EventDispatcher ? $this->getListenersWithPriority() : $this->getListenersWithoutPriority(); } catch (\Exception $e) { - if (null !== $this->logger) { - $this->logger->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); - } + $this->logger?->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); // unable to retrieve the uncalled listeners return []; @@ -219,23 +196,24 @@ public function getNotCalledListeners(Request $request = null) } $notCalled = []; + foreach ($allListeners as $eventName => $listeners) { - foreach ($listeners as $listener) { + foreach ($listeners as [$listener, $priority]) { if (!\in_array($listener, $calledListeners, true)) { if (!$listener instanceof WrappedListener) { - $listener = new WrappedListener($listener, null, $this->stopwatch, $this); + $listener = new WrappedListener($listener, null, $this->stopwatch, $this, $priority); } $notCalled[] = $listener->getInfo($eventName); } } } - uasort($notCalled, [$this, 'sortNotCalledListeners']); + uasort($notCalled, $this->sortNotCalledListeners(...)); return $notCalled; } - public function getOrphanedEvents(Request $request = null): array + public function getOrphanedEvents(?Request $request = null): array { if ($request) { return $this->orphanedEvents[spl_object_hash($request)] ?? []; @@ -248,6 +226,9 @@ public function getOrphanedEvents(Request $request = null): array return array_merge(...array_values($this->orphanedEvents)); } + /** + * @return void + */ public function reset() { $this->callStack = null; @@ -260,16 +241,16 @@ public function reset() * * @param string $method The method name * @param array $arguments The method arguments - * - * @return mixed */ - public function __call(string $method, array $arguments) + public function __call(string $method, array $arguments): mixed { return $this->dispatcher->{$method}(...$arguments); } /** * Called before dispatching the event. + * + * @return void */ protected function beforeDispatch(string $eventName, object $event) { @@ -277,6 +258,8 @@ protected function beforeDispatch(string $eventName, object $event) /** * Called after dispatching the event. + * + * @return void */ protected function afterDispatch(string $eventName, object $event) { @@ -318,9 +301,7 @@ private function postProcess(string $eventName): void } if ($listener->wasCalled()) { - if (null !== $this->logger) { - $this->logger->debug('Notified event "{event}" to listener "{listener}".', $context); - } + $this->logger?->debug('Notified event "{event}" to listener "{listener}".', $context); } else { $this->callStack->detach($listener); } @@ -330,16 +311,14 @@ private function postProcess(string $eventName): void } if ($listener->stoppedPropagation()) { - if (null !== $this->logger) { - $this->logger->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); - } + $this->logger?->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); $skipped = true; } } } - private function sortNotCalledListeners(array $a, array $b) + private function sortNotCalledListeners(array $a, array $b): int { if (0 !== $cmp = strcmp($a['event'], $b['event'])) { return $cmp; @@ -363,4 +342,34 @@ private function sortNotCalledListeners(array $a, array $b) return 1; } + + private function getListenersWithPriority(): array + { + $result = []; + + $allListeners = new \ReflectionProperty(EventDispatcher::class, 'listeners'); + + foreach ($allListeners->getValue($this->dispatcher) as $eventName => $listenersByPriority) { + foreach ($listenersByPriority as $priority => $listeners) { + foreach ($listeners as $listener) { + $result[$eventName][] = [$listener, $priority]; + } + } + } + + return $result; + } + + private function getListenersWithoutPriority(): array + { + $result = []; + + foreach ($this->getListeners() as $eventName => $listeners) { + foreach ($listeners as $listener) { + $result[$eventName][] = [$listener, null]; + } + } + + return $result; + } } diff --git a/symfony/event-dispatcher/Debug/WrappedListener.php b/symfony/event-dispatcher/Debug/WrappedListener.php index 3c4cc1335..59f7c1362 100644 --- a/symfony/event-dispatcher/Debug/WrappedListener.php +++ b/symfony/event-dispatcher/Debug/WrappedListener.php @@ -21,33 +21,34 @@ */ final class WrappedListener { - private $listener; - private $optimizedListener; - private $name; - private $called; - private $stoppedPropagation; - private $stopwatch; - private $dispatcher; - private $pretty; - private $stub; - private $priority; - private static $hasClassStub; - - public function __construct($listener, ?string $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null) + private string|array|object $listener; + private ?\Closure $optimizedListener; + private string $name; + private bool $called = false; + private bool $stoppedPropagation = false; + private Stopwatch $stopwatch; + private ?EventDispatcherInterface $dispatcher; + private string $pretty; + private string $callableRef; + private ClassStub|string $stub; + private ?int $priority = null; + private static bool $hasClassStub; + + public function __construct(callable|array $listener, ?string $name, Stopwatch $stopwatch, ?EventDispatcherInterface $dispatcher = null, ?int $priority = null) { $this->listener = $listener; - $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? \Closure::fromCallable($listener) : null); + $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? $listener(...) : null); $this->stopwatch = $stopwatch; $this->dispatcher = $dispatcher; - $this->called = false; - $this->stoppedPropagation = false; + $this->priority = $priority; if (\is_array($listener)) { - $this->name = \is_object($listener[0]) ? get_debug_type($listener[0]) : $listener[0]; + [$this->name, $this->callableRef] = $this->parseListener($listener); $this->pretty = $this->name.'::'.$listener[1]; + $this->callableRef .= '::'.$listener[1]; } elseif ($listener instanceof \Closure) { $r = new \ReflectionFunction($listener); - if (str_contains($r->name, '{closure}')) { + if (str_contains($r->name, '{closure')) { $this->pretty = $this->name = 'closure'; } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $this->name = $class->name; @@ -60,18 +61,17 @@ public function __construct($listener, ?string $name, Stopwatch $stopwatch, Even } else { $this->name = get_debug_type($listener); $this->pretty = $this->name.'::__invoke'; + $this->callableRef = $listener::class.'::__invoke'; } if (null !== $name) { $this->name = $name; } - if (null === self::$hasClassStub) { - self::$hasClassStub = class_exists(ClassStub::class); - } + self::$hasClassStub ??= class_exists(ClassStub::class); } - public function getWrappedListener() + public function getWrappedListener(): callable|array { return $this->listener; } @@ -93,13 +93,11 @@ public function getPretty(): string public function getInfo(string $eventName): array { - if (null === $this->stub) { - $this->stub = self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->listener) : $this->pretty.'()'; - } + $this->stub ??= self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->callableRef ?? $this->listener) : $this->pretty.'()'; return [ 'event' => $eventName, - 'priority' => null !== $this->priority ? $this->priority : (null !== $this->dispatcher ? $this->dispatcher->getListenerPriority($eventName, $this->listener) : null), + 'priority' => $this->priority ??= $this->dispatcher?->getListenerPriority($eventName, $this->listener), 'pretty' => $this->pretty, 'stub' => $this->stub, ]; @@ -110,7 +108,7 @@ public function __invoke(object $event, string $eventName, EventDispatcherInterf $dispatcher = $this->dispatcher ?: $dispatcher; $this->called = true; - $this->priority = $dispatcher->getListenerPriority($eventName, $this->listener); + $this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener); $e = $this->stopwatch->start($this->name, 'event_listener'); @@ -126,4 +124,21 @@ public function __invoke(object $event, string $eventName, EventDispatcherInterf $this->stoppedPropagation = true; } } + + private function parseListener(array $listener): array + { + if ($listener[0] instanceof \Closure) { + foreach ((new \ReflectionFunction($listener[0]))->getAttributes(\Closure::class) as $attribute) { + if ($name = $attribute->getArguments()['name'] ?? false) { + return [$name, $attribute->getArguments()['class'] ?? $name]; + } + } + } + + if (\is_object($listener[0])) { + return [get_debug_type($listener[0]), $listener[0]::class]; + } + + return [$listener[0], $listener[0]]; + } } diff --git a/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php b/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php index 6e7292b4a..13b4336aa 100644 --- a/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php +++ b/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php @@ -21,25 +21,19 @@ */ class AddEventAliasesPass implements CompilerPassInterface { - private $eventAliases; - private $eventAliasesParameter; + private array $eventAliases; - public function __construct(array $eventAliases, string $eventAliasesParameter = 'event_dispatcher.event_aliases') + public function __construct(array $eventAliases) { - if (1 < \func_num_args()) { - trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); - } - $this->eventAliases = $eventAliases; - $this->eventAliasesParameter = $eventAliasesParameter; } public function process(ContainerBuilder $container): void { - $eventAliases = $container->hasParameter($this->eventAliasesParameter) ? $container->getParameter($this->eventAliasesParameter) : []; + $eventAliases = $container->hasParameter('event_dispatcher.event_aliases') ? $container->getParameter('event_dispatcher.event_aliases') : []; $container->setParameter( - $this->eventAliasesParameter, + 'event_dispatcher.event_aliases', array_merge($eventAliases, $this->eventAliases) ); } diff --git a/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php b/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php index 5f44ff090..866f4e64f 100644 --- a/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php +++ b/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php @@ -25,84 +25,58 @@ */ class RegisterListenersPass implements CompilerPassInterface { - protected $dispatcherService; - protected $listenerTag; - protected $subscriberTag; - protected $eventAliasesParameter; - - private $hotPathEvents = []; - private $hotPathTagName = 'container.hot_path'; - private $noPreloadEvents = []; - private $noPreloadTagName = 'container.no_preload'; - - public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber', string $eventAliasesParameter = 'event_dispatcher.event_aliases') - { - if (0 < \func_num_args()) { - trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); - } - - $this->dispatcherService = $dispatcherService; - $this->listenerTag = $listenerTag; - $this->subscriberTag = $subscriberTag; - $this->eventAliasesParameter = $eventAliasesParameter; - } + private array $hotPathEvents = []; + private array $noPreloadEvents = []; /** * @return $this */ - public function setHotPathEvents(array $hotPathEvents) + public function setHotPathEvents(array $hotPathEvents): static { $this->hotPathEvents = array_flip($hotPathEvents); - if (1 < \func_num_args()) { - trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__); - $this->hotPathTagName = func_get_arg(1); - } - return $this; } /** * @return $this */ - public function setNoPreloadEvents(array $noPreloadEvents): self + public function setNoPreloadEvents(array $noPreloadEvents): static { $this->noPreloadEvents = array_flip($noPreloadEvents); - if (1 < \func_num_args()) { - trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__); - $this->noPreloadTagName = func_get_arg(1); - } - return $this; } + /** + * @return void + */ public function process(ContainerBuilder $container) { - if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { + if (!$container->hasDefinition('event_dispatcher') && !$container->hasAlias('event_dispatcher')) { return; } $aliases = []; - if ($container->hasParameter($this->eventAliasesParameter)) { - $aliases = $container->getParameter($this->eventAliasesParameter); + if ($container->hasParameter('event_dispatcher.event_aliases')) { + $aliases = $container->getParameter('event_dispatcher.event_aliases'); } - $globalDispatcherDefinition = $container->findDefinition($this->dispatcherService); + $globalDispatcherDefinition = $container->findDefinition('event_dispatcher'); - foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) { + foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) { $noPreload = 0; foreach ($events as $event) { $priority = $event['priority'] ?? 0; if (!isset($event['event'])) { - if ($container->getDefinition($id)->hasTag($this->subscriberTag)) { + if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) { continue; } - $event['method'] = $event['method'] ?? '__invoke'; + $event['method'] ??= '__invoke'; $event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']); } @@ -112,12 +86,12 @@ public function process(ContainerBuilder $container) $event['method'] = 'on'.preg_replace_callback([ '/(?<=\b|_)[a-z]/i', '/[^a-z0-9]/i', - ], function ($matches) { return strtoupper($matches[0]); }, $event['event']); + ], fn ($matches) => strtoupper($matches[0]), $event['event']); $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) { if (!$r->hasMethod('__invoke')) { - throw new InvalidArgumentException(sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "%s" tags.', $event['method'], $id, $this->listenerTag)); + throw new InvalidArgumentException(sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "kernel.event_listener" tags.', $event['method'], $id)); } $event['method'] = '__invoke'; @@ -132,20 +106,20 @@ public function process(ContainerBuilder $container) $dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); if (isset($this->hotPathEvents[$event['event']])) { - $container->getDefinition($id)->addTag($this->hotPathTagName); + $container->getDefinition($id)->addTag('container.hot_path'); } elseif (isset($this->noPreloadEvents[$event['event']])) { ++$noPreload; } } if ($noPreload && \count($events) === $noPreload) { - $container->getDefinition($id)->addTag($this->noPreloadTagName); + $container->getDefinition($id)->addTag('container.no_preload'); } } $extractingDispatcher = new ExtractingEventDispatcher(); - foreach ($container->findTaggedServiceIds($this->subscriberTag, true) as $id => $tags) { + foreach ($container->findTaggedServiceIds('kernel.event_subscriber', true) as $id => $tags) { $def = $container->getDefinition($id); // We must assume that the class value has been correctly filled, even if the service is created by a factory @@ -183,13 +157,13 @@ public function process(ContainerBuilder $container) } if (isset($this->hotPathEvents[$args[0]])) { - $container->getDefinition($id)->addTag($this->hotPathTagName); + $container->getDefinition($id)->addTag('container.hot_path'); } elseif (isset($this->noPreloadEvents[$args[0]])) { ++$noPreload; } } if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) { - $container->getDefinition($id)->addTag($this->noPreloadTagName); + $container->getDefinition($id)->addTag('container.no_preload'); } $extractingDispatcher->listeners = []; ExtractingEventDispatcher::$aliases = []; @@ -207,7 +181,7 @@ private function getEventFromTypeDeclaration(ContainerBuilder $container, string || $type->isBuiltin() || Event::class === ($name = $type->getName()) ) { - throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "kernel.event_listener" tags.', $id)); } return $name; @@ -219,12 +193,12 @@ private function getEventFromTypeDeclaration(ContainerBuilder $container, string */ class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface { - public $listeners = []; + public array $listeners = []; - public static $aliases = []; - public static $subscriber; + public static array $aliases = []; + public static string $subscriber; - public function addListener(string $eventName, $listener, int $priority = 0) + public function addListener(string $eventName, callable|array $listener, int $priority = 0): void { $this->listeners[] = [$eventName, $listener[1], $priority]; } diff --git a/symfony/event-dispatcher/EventDispatcher.php b/symfony/event-dispatcher/EventDispatcher.php index 8fe8fb5c2..605298926 100644 --- a/symfony/event-dispatcher/EventDispatcher.php +++ b/symfony/event-dispatcher/EventDispatcher.php @@ -31,9 +31,9 @@ */ class EventDispatcher implements EventDispatcherInterface { - private $listeners = []; - private $sorted = []; - private $optimized; + private array $listeners = []; + private array $sorted = []; + private array $optimized; public function __construct() { @@ -42,14 +42,11 @@ public function __construct() } } - /** - * {@inheritdoc} - */ - public function dispatch(object $event, string $eventName = null): object + public function dispatch(object $event, ?string $eventName = null): object { - $eventName = $eventName ?? \get_class($event); + $eventName ??= $event::class; - if (null !== $this->optimized) { + if (isset($this->optimized)) { $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName)); } else { $listeners = $this->getListeners($eventName); @@ -62,10 +59,7 @@ public function dispatch(object $event, string $eventName = null): object return $event; } - /** - * {@inheritdoc} - */ - public function getListeners(string $eventName = null) + public function getListeners(?string $eventName = null): array { if (null !== $eventName) { if (empty($this->listeners[$eventName])) { @@ -88,10 +82,7 @@ public function getListeners(string $eventName = null) return array_filter($this->sorted); } - /** - * {@inheritdoc} - */ - public function getListenerPriority(string $eventName, $listener) + public function getListenerPriority(string $eventName, callable|array $listener): ?int { if (empty($this->listeners[$eventName])) { return null; @@ -99,14 +90,14 @@ public function getListenerPriority(string $eventName, $listener) if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); - $listener[1] = $listener[1] ?? '__invoke'; + $listener[1] ??= '__invoke'; } foreach ($this->listeners[$eventName] as $priority => &$listeners) { foreach ($listeners as &$v) { if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { $v[0] = $v[0](); - $v[1] = $v[1] ?? '__invoke'; + $v[1] ??= '__invoke'; } if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { return $priority; @@ -117,10 +108,7 @@ public function getListenerPriority(string $eventName, $listener) return null; } - /** - * {@inheritdoc} - */ - public function hasListeners(string $eventName = null) + public function hasListeners(?string $eventName = null): bool { if (null !== $eventName) { return !empty($this->listeners[$eventName]); @@ -136,18 +124,18 @@ public function hasListeners(string $eventName = null) } /** - * {@inheritdoc} + * @return void */ - public function addListener(string $eventName, $listener, int $priority = 0) + public function addListener(string $eventName, callable|array $listener, int $priority = 0) { $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName], $this->optimized[$eventName]); } /** - * {@inheritdoc} + * @return void */ - public function removeListener(string $eventName, $listener) + public function removeListener(string $eventName, callable|array $listener) { if (empty($this->listeners[$eventName])) { return; @@ -155,14 +143,14 @@ public function removeListener(string $eventName, $listener) if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); - $listener[1] = $listener[1] ?? '__invoke'; + $listener[1] ??= '__invoke'; } foreach ($this->listeners[$eventName] as $priority => &$listeners) { foreach ($listeners as $k => &$v) { if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { $v[0] = $v[0](); - $v[1] = $v[1] ?? '__invoke'; + $v[1] ??= '__invoke'; } if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]); @@ -176,7 +164,7 @@ public function removeListener(string $eventName, $listener) } /** - * {@inheritdoc} + * @return void */ public function addSubscriber(EventSubscriberInterface $subscriber) { @@ -194,7 +182,7 @@ public function addSubscriber(EventSubscriberInterface $subscriber) } /** - * {@inheritdoc} + * @return void */ public function removeSubscriber(EventSubscriberInterface $subscriber) { @@ -218,6 +206,8 @@ public function removeSubscriber(EventSubscriberInterface $subscriber) * @param callable[] $listeners The event listeners * @param string $eventName The name of the event to dispatch * @param object $event The event object to pass to the event handlers/listeners + * + * @return void */ protected function callListeners(iterable $listeners, string $eventName, object $event) { @@ -234,16 +224,16 @@ protected function callListeners(iterable $listeners, string $eventName, object /** * Sorts the internal list of listeners for the given event by priority. */ - private function sortListeners(string $eventName) + private function sortListeners(string $eventName): void { krsort($this->listeners[$eventName]); $this->sorted[$eventName] = []; foreach ($this->listeners[$eventName] as &$listeners) { - foreach ($listeners as $k => &$listener) { + foreach ($listeners as &$listener) { if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); - $listener[1] = $listener[1] ?? '__invoke'; + $listener[1] ??= '__invoke'; } $this->sorted[$eventName][] = $listener; } @@ -265,12 +255,12 @@ private function optimizeListeners(string $eventName): array $closure = static function (...$args) use (&$listener, &$closure) { if ($listener[0] instanceof \Closure) { $listener[0] = $listener[0](); - $listener[1] = $listener[1] ?? '__invoke'; + $listener[1] ??= '__invoke'; } - ($closure = \Closure::fromCallable($listener))(...$args); + ($closure = $listener(...))(...$args); }; } else { - $closure = $listener instanceof \Closure || $listener instanceof WrappedListener ? $listener : \Closure::fromCallable($listener); + $closure = $listener instanceof WrappedListener ? $listener : $listener(...); } } } diff --git a/symfony/event-dispatcher/EventDispatcherInterface.php b/symfony/event-dispatcher/EventDispatcherInterface.php index cc324e1c6..e95a7b11d 100644 --- a/symfony/event-dispatcher/EventDispatcherInterface.php +++ b/symfony/event-dispatcher/EventDispatcherInterface.php @@ -27,6 +27,8 @@ interface EventDispatcherInterface extends ContractsEventDispatcherInterface * * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 0) + * + * @return void */ public function addListener(string $eventName, callable $listener, int $priority = 0); @@ -35,14 +37,21 @@ public function addListener(string $eventName, callable $listener, int $priority * * The subscriber is asked for all the events it is * interested in and added as a listener for these events. + * + * @return void */ public function addSubscriber(EventSubscriberInterface $subscriber); /** * Removes an event listener from the specified events. + * + * @return void */ public function removeListener(string $eventName, callable $listener); + /** + * @return void + */ public function removeSubscriber(EventSubscriberInterface $subscriber); /** @@ -50,21 +59,17 @@ public function removeSubscriber(EventSubscriberInterface $subscriber); * * @return array */ - public function getListeners(string $eventName = null); + public function getListeners(?string $eventName = null): array; /** * Gets the listener priority for a specific event. * * Returns null if the event or the listener does not exist. - * - * @return int|null */ - public function getListenerPriority(string $eventName, callable $listener); + public function getListenerPriority(string $eventName, callable $listener): ?int; /** * Checks whether an event has any registered listeners. - * - * @return bool */ - public function hasListeners(string $eventName = null); + public function hasListeners(?string $eventName = null): bool; } diff --git a/symfony/event-dispatcher/GenericEvent.php b/symfony/event-dispatcher/GenericEvent.php index b32a301ae..0ccbbd810 100644 --- a/symfony/event-dispatcher/GenericEvent.php +++ b/symfony/event-dispatcher/GenericEvent.php @@ -29,12 +29,12 @@ class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate protected $arguments; /** - * Encapsulate an event with $subject and $args. + * Encapsulate an event with $subject and $arguments. * * @param mixed $subject The subject of the event, usually an object or a callable * @param array $arguments Arguments to store in the event */ - public function __construct($subject = null, array $arguments = []) + public function __construct(mixed $subject = null, array $arguments = []) { $this->subject = $subject; $this->arguments = $arguments; @@ -42,10 +42,8 @@ public function __construct($subject = null, array $arguments = []) /** * Getter for subject property. - * - * @return mixed */ - public function getSubject() + public function getSubject(): mixed { return $this->subject; } @@ -53,11 +51,9 @@ public function getSubject() /** * Get argument by key. * - * @return mixed - * * @throws \InvalidArgumentException if key is not found */ - public function getArgument(string $key) + public function getArgument(string $key): mixed { if ($this->hasArgument($key)) { return $this->arguments[$key]; @@ -69,11 +65,9 @@ public function getArgument(string $key) /** * Add argument to event. * - * @param mixed $value Value - * * @return $this */ - public function setArgument(string $key, $value) + public function setArgument(string $key, mixed $value): static { $this->arguments[$key] = $value; @@ -82,10 +76,8 @@ public function setArgument(string $key, $value) /** * Getter for all arguments. - * - * @return array */ - public function getArguments() + public function getArguments(): array { return $this->arguments; } @@ -95,7 +87,7 @@ public function getArguments() * * @return $this */ - public function setArguments(array $args = []) + public function setArguments(array $args = []): static { $this->arguments = $args; @@ -104,10 +96,8 @@ public function setArguments(array $args = []) /** * Has argument. - * - * @return bool */ - public function hasArgument(string $key) + public function hasArgument(string $key): bool { return \array_key_exists($key, $this->arguments); } @@ -117,12 +107,9 @@ public function hasArgument(string $key) * * @param string $key Array key * - * @return mixed - * * @throws \InvalidArgumentException if key does not exist in $this->args */ - #[\ReturnTypeWillChange] - public function offsetGet($key) + public function offsetGet(mixed $key): mixed { return $this->getArgument($key); } @@ -130,13 +117,9 @@ public function offsetGet($key) /** * ArrayAccess for argument setter. * - * @param string $key Array key to set - * @param mixed $value Value - * - * @return void + * @param string $key Array key to set */ - #[\ReturnTypeWillChange] - public function offsetSet($key, $value) + public function offsetSet(mixed $key, mixed $value): void { $this->setArgument($key, $value); } @@ -145,11 +128,8 @@ public function offsetSet($key, $value) * ArrayAccess for unset argument. * * @param string $key Array key - * - * @return void */ - #[\ReturnTypeWillChange] - public function offsetUnset($key) + public function offsetUnset(mixed $key): void { if ($this->hasArgument($key)) { unset($this->arguments[$key]); @@ -160,11 +140,8 @@ public function offsetUnset($key) * ArrayAccess has argument. * * @param string $key Array key - * - * @return bool */ - #[\ReturnTypeWillChange] - public function offsetExists($key) + public function offsetExists(mixed $key): bool { return $this->hasArgument($key); } @@ -174,8 +151,7 @@ public function offsetExists($key) * * @return \ArrayIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->arguments); } diff --git a/symfony/event-dispatcher/ImmutableEventDispatcher.php b/symfony/event-dispatcher/ImmutableEventDispatcher.php index 568d79c3a..301a805cb 100644 --- a/symfony/event-dispatcher/ImmutableEventDispatcher.php +++ b/symfony/event-dispatcher/ImmutableEventDispatcher.php @@ -18,31 +18,28 @@ */ class ImmutableEventDispatcher implements EventDispatcherInterface { - private $dispatcher; + private EventDispatcherInterface $dispatcher; public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } - /** - * {@inheritdoc} - */ - public function dispatch(object $event, string $eventName = null): object + public function dispatch(object $event, ?string $eventName = null): object { return $this->dispatcher->dispatch($event, $eventName); } /** - * {@inheritdoc} + * @return never */ - public function addListener(string $eventName, $listener, int $priority = 0) + public function addListener(string $eventName, callable|array $listener, int $priority = 0) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** - * {@inheritdoc} + * @return never */ public function addSubscriber(EventSubscriberInterface $subscriber) { @@ -50,41 +47,32 @@ public function addSubscriber(EventSubscriberInterface $subscriber) } /** - * {@inheritdoc} + * @return never */ - public function removeListener(string $eventName, $listener) + public function removeListener(string $eventName, callable|array $listener) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** - * {@inheritdoc} + * @return never */ public function removeSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } - /** - * {@inheritdoc} - */ - public function getListeners(string $eventName = null) + public function getListeners(?string $eventName = null): array { return $this->dispatcher->getListeners($eventName); } - /** - * {@inheritdoc} - */ - public function getListenerPriority(string $eventName, $listener) + public function getListenerPriority(string $eventName, callable|array $listener): ?int { return $this->dispatcher->getListenerPriority($eventName, $listener); } - /** - * {@inheritdoc} - */ - public function hasListeners(string $eventName = null) + public function hasListeners(?string $eventName = null): bool { return $this->dispatcher->hasListeners($eventName); } diff --git a/symfony/event-dispatcher/LegacyEventDispatcherProxy.php b/symfony/event-dispatcher/LegacyEventDispatcherProxy.php deleted file mode 100644 index 6e17c8fcc..000000000 --- a/symfony/event-dispatcher/LegacyEventDispatcherProxy.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\EventDispatcher; - -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; - -trigger_deprecation('symfony/event-dispatcher', '5.1', '%s is deprecated, use the event dispatcher without the proxy.', LegacyEventDispatcherProxy::class); - -/** - * A helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch(). - * - * @author Nicolas Grekas - * - * @deprecated since Symfony 5.1 - */ -final class LegacyEventDispatcherProxy -{ - public static function decorate(?EventDispatcherInterface $dispatcher): ?EventDispatcherInterface - { - return $dispatcher; - } -} diff --git a/symfony/http-foundation/AcceptHeader.php b/symfony/http-foundation/AcceptHeader.php index 057c6b530..853c000e0 100644 --- a/symfony/http-foundation/AcceptHeader.php +++ b/symfony/http-foundation/AcceptHeader.php @@ -27,12 +27,9 @@ class AcceptHeader /** * @var AcceptHeaderItem[] */ - private $items = []; + private array $items = []; - /** - * @var bool - */ - private $sorted = true; + private bool $sorted = true; /** * @param AcceptHeaderItem[] $items @@ -46,16 +43,13 @@ public function __construct(array $items) /** * Builds an AcceptHeader instance from a string. - * - * @return self */ - public static function fromString(?string $headerValue) + public static function fromString(?string $headerValue): self { - $index = 0; - $parts = HeaderUtils::split($headerValue ?? '', ',;='); - return new self(array_map(function ($subParts) use (&$index) { + return new self(array_map(function ($subParts) { + static $index = 0; $part = array_shift($subParts); $attributes = HeaderUtils::combine($subParts); @@ -68,30 +62,24 @@ public static function fromString(?string $headerValue) /** * Returns header value's string representation. - * - * @return string */ - public function __toString() + public function __toString(): string { return implode(',', $this->items); } /** * Tests if header has given value. - * - * @return bool */ - public function has(string $value) + public function has(string $value): bool { return isset($this->items[$value]); } /** * Returns given value's item, if exists. - * - * @return AcceptHeaderItem|null */ - public function get(string $value) + public function get(string $value): ?AcceptHeaderItem { return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null; } @@ -101,7 +89,7 @@ public function get(string $value) * * @return $this */ - public function add(AcceptHeaderItem $item) + public function add(AcceptHeaderItem $item): static { $this->items[$item->getValue()] = $item; $this->sorted = false; @@ -114,7 +102,7 @@ public function add(AcceptHeaderItem $item) * * @return AcceptHeaderItem[] */ - public function all() + public function all(): array { $this->sort(); @@ -123,26 +111,20 @@ public function all() /** * Filters items on their value using given regex. - * - * @return self */ - public function filter(string $pattern) + public function filter(string $pattern): self { - return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) { - return preg_match($pattern, $item->getValue()); - })); + return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue()))); } /** * Returns first item. - * - * @return AcceptHeaderItem|null */ - public function first() + public function first(): ?AcceptHeaderItem { $this->sort(); - return !empty($this->items) ? reset($this->items) : null; + return $this->items ? reset($this->items) : null; } /** diff --git a/symfony/http-foundation/AcceptHeaderItem.php b/symfony/http-foundation/AcceptHeaderItem.php index 8b86eee67..35ecd4ea2 100644 --- a/symfony/http-foundation/AcceptHeaderItem.php +++ b/symfony/http-foundation/AcceptHeaderItem.php @@ -18,10 +18,10 @@ */ class AcceptHeaderItem { - private $value; - private $quality = 1.0; - private $index = 0; - private $attributes = []; + private string $value; + private float $quality = 1.0; + private int $index = 0; + private array $attributes = []; public function __construct(string $value, array $attributes = []) { @@ -33,10 +33,8 @@ public function __construct(string $value, array $attributes = []) /** * Builds an AcceptHeaderInstance instance from a string. - * - * @return self */ - public static function fromString(?string $itemValue) + public static function fromString(?string $itemValue): self { $parts = HeaderUtils::split($itemValue ?? '', ';='); @@ -48,10 +46,8 @@ public static function fromString(?string $itemValue) /** * Returns header value's string representation. - * - * @return string */ - public function __toString() + public function __toString(): string { $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); if (\count($this->attributes) > 0) { @@ -66,7 +62,7 @@ public function __toString() * * @return $this */ - public function setValue(string $value) + public function setValue(string $value): static { $this->value = $value; @@ -75,10 +71,8 @@ public function setValue(string $value) /** * Returns the item value. - * - * @return string */ - public function getValue() + public function getValue(): string { return $this->value; } @@ -88,7 +82,7 @@ public function getValue() * * @return $this */ - public function setQuality(float $quality) + public function setQuality(float $quality): static { $this->quality = $quality; @@ -97,10 +91,8 @@ public function setQuality(float $quality) /** * Returns the item quality. - * - * @return float */ - public function getQuality() + public function getQuality(): float { return $this->quality; } @@ -110,7 +102,7 @@ public function getQuality() * * @return $this */ - public function setIndex(int $index) + public function setIndex(int $index): static { $this->index = $index; @@ -119,42 +111,32 @@ public function setIndex(int $index) /** * Returns the item index. - * - * @return int */ - public function getIndex() + public function getIndex(): int { return $this->index; } /** * Tests if an attribute exists. - * - * @return bool */ - public function hasAttribute(string $name) + public function hasAttribute(string $name): bool { return isset($this->attributes[$name]); } /** * Returns an attribute by its name. - * - * @param mixed $default - * - * @return mixed */ - public function getAttribute(string $name, $default = null) + public function getAttribute(string $name, mixed $default = null): mixed { return $this->attributes[$name] ?? $default; } /** * Returns all attributes. - * - * @return array */ - public function getAttributes() + public function getAttributes(): array { return $this->attributes; } @@ -164,7 +146,7 @@ public function getAttributes() * * @return $this */ - public function setAttribute(string $name, string $value) + public function setAttribute(string $name, string $value): static { if ('q' === $name) { $this->quality = (float) $value; diff --git a/symfony/http-foundation/BinaryFileResponse.php b/symfony/http-foundation/BinaryFileResponse.php index d3caa36aa..41a244b81 100644 --- a/symfony/http-foundation/BinaryFileResponse.php +++ b/symfony/http-foundation/BinaryFileResponse.php @@ -38,14 +38,14 @@ class BinaryFileResponse extends Response /** * @param \SplFileInfo|string $file The file to stream - * @param int $status The response status code + * @param int $status The response status code (200 "OK" by default) * @param array $headers An array of response headers * @param bool $public Files are public by default * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename * @param bool $autoEtag Whether the ETag header should be automatically set * @param bool $autoLastModified Whether the Last-Modified header should be automatically set */ - public function __construct($file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + public function __construct(\SplFileInfo|string $file, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) { parent::__construct(null, $status, $headers); @@ -56,36 +56,14 @@ public function __construct($file, int $status = 200, array $headers = [], bool } } - /** - * @param \SplFileInfo|string $file The file to stream - * @param int $status The response status code - * @param array $headers An array of response headers - * @param bool $public Files are public by default - * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename - * @param bool $autoEtag Whether the ETag header should be automatically set - * @param bool $autoLastModified Whether the Last-Modified header should be automatically set - * - * @return static - * - * @deprecated since Symfony 5.2, use __construct() instead. - */ - public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) - { - trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); - - return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified); - } - /** * Sets the file to stream. * - * @param \SplFileInfo|string $file The file to stream - * * @return $this * * @throws FileException */ - public function setFile($file, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + public function setFile(\SplFileInfo|string $file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): static { if (!$file instanceof File) { if ($file instanceof \SplFileInfo) { @@ -118,10 +96,8 @@ public function setFile($file, string $contentDisposition = null, bool $autoEtag /** * Gets the file. - * - * @return File */ - public function getFile() + public function getFile(): File { return $this->file; } @@ -131,7 +107,7 @@ public function getFile() * * @return $this */ - public function setChunkSize(int $chunkSize): self + public function setChunkSize(int $chunkSize): static { if ($chunkSize < 1 || $chunkSize > \PHP_INT_MAX) { throw new \LogicException('The chunk size of a BinaryFileResponse cannot be less than 1 or greater than PHP_INT_MAX.'); @@ -147,9 +123,9 @@ public function setChunkSize(int $chunkSize): self * * @return $this */ - public function setAutoLastModified() + public function setAutoLastModified(): static { - $this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime())); + $this->setLastModified(\DateTimeImmutable::createFromFormat('U', $this->file->getMTime())); return $this; } @@ -159,7 +135,7 @@ public function setAutoLastModified() * * @return $this */ - public function setAutoEtag() + public function setAutoEtag(): static { $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); @@ -175,7 +151,7 @@ public function setAutoEtag() * * @return $this */ - public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = '') + public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = ''): static { if ('' === $filename) { $filename = $this->file->getFilename(); @@ -201,10 +177,7 @@ public function setContentDisposition(string $disposition, string $filename = '' return $this; } - /** - * {@inheritdoc} - */ - public function prepare(Request $request) + public function prepare(Request $request): static { if ($this->isInformational() || $this->isEmpty()) { parent::prepare($request); @@ -244,11 +217,15 @@ public function prepare(Request $request) } if ('x-accel-redirect' === strtolower($type)) { // Do X-Accel-Mapping substitutions. - // @link https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-redirect - $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',='); + // @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb + // @link https://mattbrictson.com/blog/accelerated-rails-downloads + if (!$request->headers->has('X-Accel-Mapping')) { + throw new \LogicException('The "X-Accel-Mapping" header must be set when "X-Sendfile-Type" is set to "X-Accel-Redirect".'); + } + $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping'), ',='); foreach ($parts as $part) { [$pathPrefix, $location] = $part; - if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) { + if (str_starts_with($path, $pathPrefix)) { $path = $location.substr($path, \strlen($pathPrefix)); // Only set X-Accel-Redirect header if a valid URI can be produced // as nginx does not serve arbitrary file paths. @@ -316,14 +293,11 @@ private function hasValidIfRangeHeader(?string $header): bool return $lastModified->format('D, d M Y H:i:s').' GMT' === $header; } - /** - * {@inheritdoc} - */ - public function sendContent() + public function sendContent(): static { try { if (!$this->isSuccessful()) { - return parent::sendContent(); + return $this; } if (0 === $this->maxlen) { @@ -370,11 +344,9 @@ public function sendContent() } /** - * {@inheritdoc} - * * @throws \LogicException when the content is not null */ - public function setContent(?string $content) + public function setContent(?string $content): static { if (null !== $content) { throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.'); @@ -383,16 +355,15 @@ public function setContent(?string $content) return $this; } - /** - * {@inheritdoc} - */ - public function getContent() + public function getContent(): string|false { return false; } /** * Trust X-Sendfile-Type header. + * + * @return void */ public static function trustXSendfileTypeHeader() { @@ -405,7 +376,7 @@ public static function trustXSendfileTypeHeader() * * @return $this */ - public function deleteFileAfterSend(bool $shouldDelete = true) + public function deleteFileAfterSend(bool $shouldDelete = true): static { $this->deleteFileAfterSend = $shouldDelete; diff --git a/symfony/http-foundation/ChainRequestMatcher.php b/symfony/http-foundation/ChainRequestMatcher.php new file mode 100644 index 000000000..29486fc8d --- /dev/null +++ b/symfony/http-foundation/ChainRequestMatcher.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ChainRequestMatcher verifies that all checks match against a Request instance. + * + * @author Fabien Potencier + */ +class ChainRequestMatcher implements RequestMatcherInterface +{ + /** + * @param iterable $matchers + */ + public function __construct(private iterable $matchers) + { + } + + public function matches(Request $request): bool + { + foreach ($this->matchers as $matcher) { + if (!$matcher->matches($request)) { + return false; + } + } + + return true; + } +} diff --git a/symfony/http-foundation/Cookie.php b/symfony/http-foundation/Cookie.php index 91024535b..4a3b73608 100644 --- a/symfony/http-foundation/Cookie.php +++ b/symfony/http-foundation/Cookie.php @@ -30,9 +30,10 @@ class Cookie protected $secure; protected $httpOnly; - private $raw; - private $sameSite; - private $secureDefault = false; + private bool $raw; + private ?string $sameSite = null; + private bool $partitioned = false; + private bool $secureDefault = false; private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; @@ -40,10 +41,8 @@ class Cookie /** * Creates cookie from raw header string. - * - * @return static */ - public static function fromString(string $cookie, bool $decode = false) + public static function fromString(string $cookie, bool $decode = false): static { $data = [ 'expires' => 0, @@ -53,6 +52,7 @@ public static function fromString(string $cookie, bool $decode = false) 'httponly' => false, 'raw' => !$decode, 'samesite' => null, + 'partitioned' => false, ]; $parts = HeaderUtils::split($cookie, ';='); @@ -68,12 +68,20 @@ public static function fromString(string $cookie, bool $decode = false) $data['expires'] = time() + (int) $data['max-age']; } - return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); + return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']); } - public static function create(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self + /** + * @see self::__construct + * + * @param self::SAMESITE_*|''|null $sameSite + * @param bool $partitioned + */ + public static function create(string $name, ?string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX /* , bool $partitioned = false */): self { - return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); + $partitioned = 9 < \func_num_args() ? func_get_arg(9) : false; + + return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned); } /** @@ -85,11 +93,11 @@ public static function create(string $name, string $value = null, $expire = 0, ? * @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol * @param bool $raw Whether the cookie value should be sent with no url encoding - * @param string|null $sameSite Whether the cookie will be available for cross-site requests + * @param self::SAMESITE_*|''|null $sameSite Whether the cookie will be available for cross-site requests * * @throws \InvalidArgumentException */ - public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax') + public function __construct(string $name, ?string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX, bool $partitioned = false) { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { @@ -109,14 +117,13 @@ public function __construct(string $name, string $value = null, $expire = 0, ?st $this->httpOnly = $httpOnly; $this->raw = $raw; $this->sameSite = $this->withSameSite($sameSite)->sameSite; + $this->partitioned = $partitioned; } /** * Creates a cookie copy with a new value. - * - * @return static */ - public function withValue(?string $value): self + public function withValue(?string $value): static { $cookie = clone $this; $cookie->value = $value; @@ -126,10 +133,8 @@ public function withValue(?string $value): self /** * Creates a cookie copy with a new domain that the cookie is available to. - * - * @return static */ - public function withDomain(?string $domain): self + public function withDomain(?string $domain): static { $cookie = clone $this; $cookie->domain = $domain; @@ -139,12 +144,8 @@ public function withDomain(?string $domain): self /** * Creates a cookie copy with a new time the cookie expires. - * - * @param int|string|\DateTimeInterface $expire - * - * @return static */ - public function withExpires($expire = 0): self + public function withExpires(int|string|\DateTimeInterface $expire = 0): static { $cookie = clone $this; $cookie->expire = self::expiresTimestamp($expire); @@ -154,10 +155,8 @@ public function withExpires($expire = 0): self /** * Converts expires formats to a unix timestamp. - * - * @param int|string|\DateTimeInterface $expire */ - private static function expiresTimestamp($expire = 0): int + private static function expiresTimestamp(int|string|\DateTimeInterface $expire = 0): int { // convert expiration time to a Unix timestamp if ($expire instanceof \DateTimeInterface) { @@ -175,10 +174,8 @@ private static function expiresTimestamp($expire = 0): int /** * Creates a cookie copy with a new path on the server in which the cookie will be available on. - * - * @return static */ - public function withPath(string $path): self + public function withPath(string $path): static { $cookie = clone $this; $cookie->path = '' === $path ? '/' : $path; @@ -188,10 +185,8 @@ public function withPath(string $path): self /** * Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client. - * - * @return static */ - public function withSecure(bool $secure = true): self + public function withSecure(bool $secure = true): static { $cookie = clone $this; $cookie->secure = $secure; @@ -201,10 +196,8 @@ public function withSecure(bool $secure = true): self /** * Creates a cookie copy that be accessible only through the HTTP protocol. - * - * @return static */ - public function withHttpOnly(bool $httpOnly = true): self + public function withHttpOnly(bool $httpOnly = true): static { $cookie = clone $this; $cookie->httpOnly = $httpOnly; @@ -214,10 +207,8 @@ public function withHttpOnly(bool $httpOnly = true): self /** * Creates a cookie copy that uses no url encoding. - * - * @return static */ - public function withRaw(bool $raw = true): self + public function withRaw(bool $raw = true): static { if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name)); @@ -232,9 +223,9 @@ public function withRaw(bool $raw = true): self /** * Creates a cookie copy with SameSite attribute. * - * @return static + * @param self::SAMESITE_*|''|null $sameSite */ - public function withSameSite(?string $sameSite): self + public function withSameSite(?string $sameSite): static { if ('' === $sameSite) { $sameSite = null; @@ -252,12 +243,21 @@ public function withSameSite(?string $sameSite): self return $cookie; } + /** + * Creates a cookie copy that is tied to the top-level site in cross-site context. + */ + public function withPartitioned(bool $partitioned = true): static + { + $cookie = clone $this; + $cookie->partitioned = $partitioned; + + return $cookie; + } + /** * Returns the cookie as a string. - * - * @return string */ - public function __toString() + public function __toString(): string { if ($this->isRaw()) { $str = $this->getName(); @@ -268,12 +268,12 @@ public function __toString() $str .= '='; if ('' === (string) $this->getValue()) { - $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; + $str .= 'deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0'; } else { $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue()); if (0 !== $this->getExpiresTime()) { - $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); + $str .= '; expires='.gmdate('D, d M Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); } } @@ -285,11 +285,11 @@ public function __toString() $str .= '; domain='.$this->getDomain(); } - if (true === $this->isSecure()) { + if ($this->isSecure()) { $str .= '; secure'; } - if (true === $this->isHttpOnly()) { + if ($this->isHttpOnly()) { $str .= '; httponly'; } @@ -297,55 +297,49 @@ public function __toString() $str .= '; samesite='.$this->getSameSite(); } + if ($this->isPartitioned()) { + $str .= '; partitioned'; + } + return $str; } /** * Gets the name of the cookie. - * - * @return string */ - public function getName() + public function getName(): string { return $this->name; } /** * Gets the value of the cookie. - * - * @return string|null */ - public function getValue() + public function getValue(): ?string { return $this->value; } /** * Gets the domain that the cookie is available to. - * - * @return string|null */ - public function getDomain() + public function getDomain(): ?string { return $this->domain; } /** * Gets the time the cookie expires. - * - * @return int */ - public function getExpiresTime() + public function getExpiresTime(): int { return $this->expire; } /** * Gets the max-age attribute. - * - * @return int */ - public function getMaxAge() + public function getMaxAge(): int { $maxAge = $this->expire - time(); @@ -354,60 +348,56 @@ public function getMaxAge() /** * Gets the path on the server in which the cookie will be available on. - * - * @return string */ - public function getPath() + public function getPath(): string { return $this->path; } /** * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. - * - * @return bool */ - public function isSecure() + public function isSecure(): bool { return $this->secure ?? $this->secureDefault; } /** * Checks whether the cookie will be made accessible only through the HTTP protocol. - * - * @return bool */ - public function isHttpOnly() + public function isHttpOnly(): bool { return $this->httpOnly; } /** * Whether this cookie is about to be cleared. - * - * @return bool */ - public function isCleared() + public function isCleared(): bool { return 0 !== $this->expire && $this->expire < time(); } /** * Checks if the cookie value should be sent with no url encoding. - * - * @return bool */ - public function isRaw() + public function isRaw(): bool { return $this->raw; } /** - * Gets the SameSite attribute. - * - * @return string|null + * Checks whether the cookie should be tied to the top-level site in cross-site context. + */ + public function isPartitioned(): bool + { + return $this->partitioned; + } + + /** + * @return self::SAMESITE_*|null */ - public function getSameSite() + public function getSameSite(): ?string { return $this->sameSite; } diff --git a/symfony/http-foundation/Exception/BadRequestException.php b/symfony/http-foundation/Exception/BadRequestException.php index e4bb309c4..505e1cfde 100644 --- a/symfony/http-foundation/Exception/BadRequestException.php +++ b/symfony/http-foundation/Exception/BadRequestException.php @@ -14,6 +14,6 @@ /** * Raised when a user sends a malformed request. */ -class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface +class BadRequestException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/symfony/http-foundation/Exception/ConflictingHeadersException.php b/symfony/http-foundation/Exception/ConflictingHeadersException.php index 5fcf5b426..77aa0e1ee 100644 --- a/symfony/http-foundation/Exception/ConflictingHeadersException.php +++ b/symfony/http-foundation/Exception/ConflictingHeadersException.php @@ -16,6 +16,6 @@ * * @author Magnus Nordlander */ -class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface +class ConflictingHeadersException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/symfony/http-foundation/Exception/JsonException.php b/symfony/http-foundation/Exception/JsonException.php index 5990e760e..6d1e0aecb 100644 --- a/symfony/http-foundation/Exception/JsonException.php +++ b/symfony/http-foundation/Exception/JsonException.php @@ -16,6 +16,6 @@ * * @author Tobias Nyholm */ -final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface +final class JsonException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/symfony/http-foundation/Exception/SessionNotFoundException.php b/symfony/http-foundation/Exception/SessionNotFoundException.php index 94b0cb69a..80a21bf15 100644 --- a/symfony/http-foundation/Exception/SessionNotFoundException.php +++ b/symfony/http-foundation/Exception/SessionNotFoundException.php @@ -20,7 +20,7 @@ */ class SessionNotFoundException extends \LogicException implements RequestExceptionInterface { - public function __construct(string $message = 'There is currently no session available.', int $code = 0, \Throwable $previous = null) + public function __construct(string $message = 'There is currently no session available.', int $code = 0, ?\Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/symfony/http-foundation/Exception/SuspiciousOperationException.php b/symfony/http-foundation/Exception/SuspiciousOperationException.php index ae7a5f133..4818ef2c8 100644 --- a/symfony/http-foundation/Exception/SuspiciousOperationException.php +++ b/symfony/http-foundation/Exception/SuspiciousOperationException.php @@ -15,6 +15,6 @@ * Raised when a user has performed an operation that should be considered * suspicious from a security perspective. */ -class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface +class SuspiciousOperationException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/symfony/http-foundation/Exception/UnexpectedValueException.php b/symfony/http-foundation/Exception/UnexpectedValueException.php new file mode 100644 index 000000000..c3e6c9d6d --- /dev/null +++ b/symfony/http-foundation/Exception/UnexpectedValueException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +class UnexpectedValueException extends \UnexpectedValueException +{ +} diff --git a/symfony/http-foundation/ExpressionRequestMatcher.php b/symfony/http-foundation/ExpressionRequestMatcher.php index 26bed7d37..fe65e920d 100644 --- a/symfony/http-foundation/ExpressionRequestMatcher.php +++ b/symfony/http-foundation/ExpressionRequestMatcher.php @@ -11,28 +11,37 @@ namespace Symfony\Component\HttpFoundation; +use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher as NewExpressionRequestMatcher; + +trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s" class is deprecated, use "%s" instead.', ExpressionRequestMatcher::class, NewExpressionRequestMatcher::class); /** * ExpressionRequestMatcher uses an expression to match a Request. * * @author Fabien Potencier + * + * @deprecated since Symfony 6.2, use "Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher" instead */ class ExpressionRequestMatcher extends RequestMatcher { - private $language; - private $expression; + private ExpressionLanguage $language; + private Expression|string $expression; - public function setExpression(ExpressionLanguage $language, $expression) + /** + * @return void + */ + public function setExpression(ExpressionLanguage $language, Expression|string $expression) { $this->language = $language; $this->expression = $expression; } - public function matches(Request $request) + public function matches(Request $request): bool { - if (!$this->language) { - throw new \LogicException('Unable to match the request as the expression language is not available.'); + if (!isset($this->language)) { + throw new \LogicException('Unable to match the request as the expression language is not available. Try running "composer require symfony/expression-language".'); } return $this->language->evaluate($this->expression, [ diff --git a/symfony/http-foundation/File/Exception/UnexpectedTypeException.php b/symfony/http-foundation/File/Exception/UnexpectedTypeException.php index 8533f99a8..905bd5962 100644 --- a/symfony/http-foundation/File/Exception/UnexpectedTypeException.php +++ b/symfony/http-foundation/File/Exception/UnexpectedTypeException.php @@ -13,7 +13,7 @@ class UnexpectedTypeException extends FileException { - public function __construct($value, string $expectedType) + public function __construct(mixed $value, string $expectedType) { parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); } diff --git a/symfony/http-foundation/File/File.php b/symfony/http-foundation/File/File.php index d941577d2..34ca5a537 100644 --- a/symfony/http-foundation/File/File.php +++ b/symfony/http-foundation/File/File.php @@ -47,12 +47,10 @@ public function __construct(string $path, bool $checkPath = true) * This method uses the mime type as guessed by getMimeType() * to guess the file extension. * - * @return string|null - * * @see MimeTypes * @see getMimeType() */ - public function guessExtension() + public function guessExtension(): ?string { if (!class_exists(MimeTypes::class)) { throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); @@ -68,11 +66,9 @@ public function guessExtension() * which uses finfo_file() then the "file" system binary, * depending on which of those are available. * - * @return string|null - * * @see MimeTypes */ - public function getMimeType() + public function getMimeType(): ?string { if (!class_exists(MimeTypes::class)) { throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".'); @@ -84,11 +80,9 @@ public function getMimeType() /** * Moves the file to a new location. * - * @return self - * * @throws FileException if the target file could not be created */ - public function move(string $directory, string $name = null) + public function move(string $directory, ?string $name = null): self { $target = $this->getTargetFile($directory, $name); @@ -118,10 +112,7 @@ public function getContent(): string return $content; } - /** - * @return self - */ - protected function getTargetFile(string $directory, string $name = null) + protected function getTargetFile(string $directory, ?string $name = null): self { if (!is_dir($directory)) { if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { @@ -138,10 +129,8 @@ protected function getTargetFile(string $directory, string $name = null) /** * Returns locale independent base name of the given path. - * - * @return string */ - protected function getName(string $name) + protected function getName(string $name): string { $originalName = str_replace('\\', '/', $name); $pos = strrpos($originalName, '/'); diff --git a/symfony/http-foundation/File/Stream.php b/symfony/http-foundation/File/Stream.php index cef3e0397..2c156b2e4 100644 --- a/symfony/http-foundation/File/Stream.php +++ b/symfony/http-foundation/File/Stream.php @@ -18,13 +18,7 @@ */ class Stream extends File { - /** - * {@inheritdoc} - * - * @return int|false - */ - #[\ReturnTypeWillChange] - public function getSize() + public function getSize(): int|false { return false; } diff --git a/symfony/http-foundation/File/UploadedFile.php b/symfony/http-foundation/File/UploadedFile.php index fcc629913..f475d028d 100644 --- a/symfony/http-foundation/File/UploadedFile.php +++ b/symfony/http-foundation/File/UploadedFile.php @@ -31,10 +31,10 @@ */ class UploadedFile extends File { - private $test; - private $originalName; - private $mimeType; - private $error; + private bool $test; + private string $originalName; + private string $mimeType; + private int $error; /** * Accepts the information of the uploaded file as provided by the PHP global $_FILES. @@ -60,7 +60,7 @@ class UploadedFile extends File * @throws FileException If file_uploads is disabled * @throws FileNotFoundException If the file does not exist */ - public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) + public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $error = null, bool $test = false) { $this->originalName = $this->getName($originalName); $this->mimeType = $mimeType ?: 'application/octet-stream'; @@ -74,11 +74,9 @@ public function __construct(string $path, string $originalName, string $mimeType * Returns the original file name. * * It is extracted from the request from which the file has been uploaded. - * Then it should not be considered as a safe value. - * - * @return string + * This should not be considered as a safe value to use for a file name on your servers. */ - public function getClientOriginalName() + public function getClientOriginalName(): string { return $this->originalName; } @@ -87,11 +85,9 @@ public function getClientOriginalName() * Returns the original file extension. * * It is extracted from the original file name that was uploaded. - * Then it should not be considered as a safe value. - * - * @return string + * This should not be considered as a safe value to use for a file name on your servers. */ - public function getClientOriginalExtension() + public function getClientOriginalExtension(): string { return pathinfo($this->originalName, \PATHINFO_EXTENSION); } @@ -105,11 +101,9 @@ public function getClientOriginalExtension() * For a trusted mime type, use getMimeType() instead (which guesses the mime * type based on the file content). * - * @return string - * * @see getMimeType() */ - public function getClientMimeType() + public function getClientMimeType(): string { return $this->mimeType; } @@ -126,12 +120,10 @@ public function getClientMimeType() * For a trusted extension, use guessExtension() instead (which guesses * the extension based on the guessed mime type for the file). * - * @return string|null - * * @see guessExtension() * @see getClientMimeType() */ - public function guessClientExtension() + public function guessClientExtension(): ?string { if (!class_exists(MimeTypes::class)) { throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); @@ -145,20 +137,16 @@ public function guessClientExtension() * * If the upload was successful, the constant UPLOAD_ERR_OK is returned. * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. - * - * @return int */ - public function getError() + public function getError(): int { return $this->error; } /** * Returns whether the file has been uploaded with HTTP and no error occurred. - * - * @return bool */ - public function isValid() + public function isValid(): bool { $isOk = \UPLOAD_ERR_OK === $this->error; @@ -168,11 +156,9 @@ public function isValid() /** * Moves the file to a new location. * - * @return File - * * @throws FileException if, for any reason, the file could not have been moved */ - public function move(string $directory, string $name = null) + public function move(string $directory, ?string $name = null): File { if ($this->isValid()) { if ($this->test) { @@ -221,7 +207,7 @@ public function move(string $directory, string $name = null) * * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX) */ - public static function getMaxFilesize() + public static function getMaxFilesize(): int|float { $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); @@ -229,12 +215,7 @@ public static function getMaxFilesize() return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); } - /** - * Returns the given size from an ini value in bytes. - * - * @return int|float Returns float if size > PHP_INT_MAX - */ - private static function parseFilesize(string $size) + private static function parseFilesize(string $size): int|float { if ('' === $size) { return 0; @@ -266,10 +247,8 @@ private static function parseFilesize(string $size) /** * Returns an informative upload error message. - * - * @return string */ - public function getErrorMessage() + public function getErrorMessage(): string { static $errors = [ \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', diff --git a/symfony/http-foundation/FileBag.php b/symfony/http-foundation/FileBag.php index ff5ab7778..b74a02e2e 100644 --- a/symfony/http-foundation/FileBag.php +++ b/symfony/http-foundation/FileBag.php @@ -32,7 +32,7 @@ public function __construct(array $parameters = []) } /** - * {@inheritdoc} + * @return void */ public function replace(array $files = []) { @@ -41,9 +41,9 @@ public function replace(array $files = []) } /** - * {@inheritdoc} + * @return void */ - public function set(string $key, $value) + public function set(string $key, mixed $value) { if (!\is_array($value) && !$value instanceof UploadedFile) { throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); @@ -53,7 +53,7 @@ public function set(string $key, $value) } /** - * {@inheritdoc} + * @return void */ public function add(array $files = []) { @@ -65,11 +65,9 @@ public function add(array $files = []) /** * Converts uploaded files to UploadedFile instances. * - * @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information - * * @return UploadedFile[]|UploadedFile|null */ - protected function convertFileInformation($file) + protected function convertFileInformation(array|UploadedFile $file): array|UploadedFile|null { if ($file instanceof UploadedFile) { return $file; @@ -86,7 +84,7 @@ protected function convertFileInformation($file) $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); } } else { - $file = array_map(function ($v) { return $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v; }, $file); + $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file); if (array_keys($keys) === $keys) { $file = array_filter($file); } @@ -106,10 +104,8 @@ protected function convertFileInformation($file) * * It's safe to pass an already converted array, in which case this method * just returns the original array unmodified. - * - * @return array */ - protected function fixPhpFilesArray(array $data) + protected function fixPhpFilesArray(array $data): array { // Remove extra key added by PHP 8.1. unset($data['full_path']); diff --git a/symfony/http-foundation/HeaderBag.php b/symfony/http-foundation/HeaderBag.php index 4683a6840..4dd777f16 100644 --- a/symfony/http-foundation/HeaderBag.php +++ b/symfony/http-foundation/HeaderBag.php @@ -18,7 +18,7 @@ * * @implements \IteratorAggregate> */ -class HeaderBag implements \IteratorAggregate, \Countable +class HeaderBag implements \IteratorAggregate, \Countable, \Stringable { protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; protected const LOWER = '-abcdefghijklmnopqrstuvwxyz'; @@ -38,10 +38,8 @@ public function __construct(array $headers = []) /** * Returns the headers as a string. - * - * @return string */ - public function __toString() + public function __toString(): string { if (!$headers = $this->all()) { return ''; @@ -65,9 +63,9 @@ public function __toString() * * @param string|null $key The name of the headers to return or null to get them all * - * @return array>|array + * @return ($key is null ? array> : list) */ - public function all(string $key = null) + public function all(?string $key = null): array { if (null !== $key) { return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? []; @@ -81,13 +79,15 @@ public function all(string $key = null) * * @return string[] */ - public function keys() + public function keys(): array { return array_keys($this->all()); } /** * Replaces the current HTTP headers by a new set. + * + * @return void */ public function replace(array $headers = []) { @@ -97,6 +97,8 @@ public function replace(array $headers = []) /** * Adds new headers the current HTTP headers set. + * + * @return void */ public function add(array $headers) { @@ -107,10 +109,8 @@ public function add(array $headers) /** * Returns the first header by name or the default one. - * - * @return string|null */ - public function get(string $key, string $default = null) + public function get(string $key, ?string $default = null): ?string { $headers = $this->all($key); @@ -130,8 +130,10 @@ public function get(string $key, string $default = null) * * @param string|string[]|null $values The value or an array of values * @param bool $replace Whether to replace the actual value or not (true by default) + * + * @return void */ - public function set(string $key, $values, bool $replace = true) + public function set(string $key, string|array|null $values, bool $replace = true) { $key = strtr($key, self::UPPER, self::LOWER); @@ -158,26 +160,24 @@ public function set(string $key, $values, bool $replace = true) /** * Returns true if the HTTP header is defined. - * - * @return bool */ - public function has(string $key) + public function has(string $key): bool { return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all()); } /** * Returns true if the given HTTP header contains the given value. - * - * @return bool */ - public function contains(string $key, string $value) + public function contains(string $key, string $value): bool { return \in_array($value, $this->all($key)); } /** * Removes a header. + * + * @return void */ public function remove(string $key) { @@ -193,17 +193,17 @@ public function remove(string $key) /** * Returns the HTTP header value converted to a date. * - * @return \DateTimeInterface|null + * @return \DateTimeImmutable|null * * @throws \RuntimeException When the HTTP header is not parseable */ - public function getDate(string $key, \DateTime $default = null) + public function getDate(string $key, ?\DateTimeInterface $default = null): ?\DateTimeInterface { if (null === $value = $this->get($key)) { - return $default; + return null !== $default ? \DateTimeImmutable::createFromInterface($default) : null; } - if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) { + if (false === $date = \DateTimeImmutable::createFromFormat(\DATE_RFC2822, $value)) { throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); } @@ -213,9 +213,9 @@ public function getDate(string $key, \DateTime $default = null) /** * Adds a custom Cache-Control directive. * - * @param bool|string $value The Cache-Control directive value + * @return void */ - public function addCacheControlDirective(string $key, $value = true) + public function addCacheControlDirective(string $key, bool|string $value = true) { $this->cacheControl[$key] = $value; @@ -224,26 +224,24 @@ public function addCacheControlDirective(string $key, $value = true) /** * Returns true if the Cache-Control directive is defined. - * - * @return bool */ - public function hasCacheControlDirective(string $key) + public function hasCacheControlDirective(string $key): bool { return \array_key_exists($key, $this->cacheControl); } /** * Returns a Cache-Control directive value by name. - * - * @return bool|string|null */ - public function getCacheControlDirective(string $key) + public function getCacheControlDirective(string $key): bool|string|null { return $this->cacheControl[$key] ?? null; } /** * Removes a Cache-Control directive. + * + * @return void */ public function removeCacheControlDirective(string $key) { @@ -257,23 +255,22 @@ public function removeCacheControlDirective(string $key) * * @return \ArrayIterator> */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->headers); } /** * Returns the number of headers. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->headers); } + /** + * @return string + */ protected function getCacheControlHeader() { ksort($this->cacheControl); @@ -283,10 +280,8 @@ protected function getCacheControlHeader() /** * Parses a Cache-Control HTTP header. - * - * @return array */ - protected function parseCacheControl(string $header) + protected function parseCacheControl(string $header): array { $parts = HeaderUtils::split($header, ',='); diff --git a/symfony/http-foundation/HeaderUtils.php b/symfony/http-foundation/HeaderUtils.php index 46b1e6aed..110896e17 100644 --- a/symfony/http-foundation/HeaderUtils.php +++ b/symfony/http-foundation/HeaderUtils.php @@ -33,17 +33,21 @@ private function __construct() * * Example: * - * HeaderUtils::split("da, en-gb;q=0.8", ",;") + * HeaderUtils::split('da, en-gb;q=0.8', ',;') * // => ['da'], ['en-gb', 'q=0.8']] * * @param string $separators List of characters to split on, ordered by - * precedence, e.g. ",", ";=", or ",;=" + * precedence, e.g. ',', ';=', or ',;=' * * @return array Nested array with as many levels as there are characters in * $separators */ public static function split(string $header, string $separators): array { + if ('' === $separators) { + throw new \InvalidArgumentException('At least one separator must be specified.'); + } + $quotedSeparators = preg_quote($separators, '/'); preg_match_all(' @@ -77,8 +81,8 @@ public static function split(string $header, string $separators): array * * Example: * - * HeaderUtils::combine([["foo", "abc"], ["bar"]]) - * // => ["foo" => "abc", "bar" => true] + * HeaderUtils::combine([['foo', 'abc'], ['bar']]) + * // => ['foo' => 'abc', 'bar' => true] */ public static function combine(array $parts): array { @@ -95,13 +99,13 @@ public static function combine(array $parts): array /** * Joins an associative array into a string for use in an HTTP header. * - * The key and value of each entry are joined with "=", and all entries + * The key and value of each entry are joined with '=', and all entries * are joined with the specified separator and an additional space (for * readability). Values are quoted if necessary. * * Example: * - * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",") + * HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',') * // => 'foo=abc, bar, baz="a b c"' */ public static function toString(array $assoc, string $separator): string @@ -252,39 +256,40 @@ public static function parseQuery(string $query, bool $ignoreBrackets = false, s private static function groupParts(array $matches, string $separators, bool $first = true): array { $separator = $separators[0]; - $partSeparators = substr($separators, 1); - + $separators = substr($separators, 1) ?: ''; $i = 0; + + if ('' === $separators && !$first) { + $parts = ['']; + + foreach ($matches as $match) { + if (!$i && isset($match['separator'])) { + $i = 1; + $parts[1] = ''; + } else { + $parts[$i] .= self::unquote($match[0]); + } + } + + return $parts; + } + + $parts = []; $partMatches = []; - $previousMatchWasSeparator = false; + foreach ($matches as $match) { - if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) { - $previousMatchWasSeparator = true; - $partMatches[$i][] = $match; - } elseif (isset($match['separator']) && $match['separator'] === $separator) { - $previousMatchWasSeparator = true; + if (($match['separator'] ?? null) === $separator) { ++$i; } else { - $previousMatchWasSeparator = false; $partMatches[$i][] = $match; } } - $parts = []; - if ($partSeparators) { - foreach ($partMatches as $matches) { - $parts[] = self::groupParts($matches, $partSeparators, false); - } - } else { - foreach ($partMatches as $matches) { - $parts[] = self::unquote($matches[0][0]); - } - - if (!$first && 2 < \count($parts)) { - $parts = [ - $parts[0], - implode($separator, \array_slice($parts, 1)), - ]; + foreach ($partMatches as $matches) { + if ('' === $separators && '' !== $unquoted = self::unquote($matches[0][0])) { + $parts[] = $unquoted; + } elseif ($groupedParts = self::groupParts($matches, $separators, false)) { + $parts[] = $groupedParts; } } diff --git a/symfony/http-foundation/InputBag.php b/symfony/http-foundation/InputBag.php index a9d3cd82a..5acf35fec 100644 --- a/symfony/http-foundation/InputBag.php +++ b/symfony/http-foundation/InputBag.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; /** * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. @@ -24,36 +25,26 @@ final class InputBag extends ParameterBag * Returns a scalar input value by name. * * @param string|int|float|bool|null $default The default value if the input key does not exist - * - * @return string|int|float|bool|null */ - public function get(string $key, $default = null) + public function get(string $key, mixed $default = null): string|int|float|bool|null { - if (null !== $default && !\is_scalar($default) && !(\is_object($default) && method_exists($default, '__toString'))) { - trigger_deprecation('symfony/http-foundation', '5.1', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a scalar or null instead.', __METHOD__); + if (null !== $default && !\is_scalar($default) && !$default instanceof \Stringable) { + throw new \InvalidArgumentException(sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default))); } $value = parent::get($key, $this); - if (null !== $value && $this !== $value && !\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { - trigger_deprecation('symfony/http-foundation', '5.1', 'Retrieving a non-scalar value from "%s()" is deprecated, and will throw a "%s" exception in Symfony 6.0, use "%s::all($key)" instead.', __METHOD__, BadRequestException::class, __CLASS__); + if (null !== $value && $this !== $value && !\is_scalar($value) && !$value instanceof \Stringable) { + throw new BadRequestException(sprintf('Input value "%s" contains a non-scalar value.', $key)); } return $this === $value ? $default : $value; } - /** - * {@inheritdoc} - */ - public function all(string $key = null): array - { - return parent::all($key); - } - /** * Replaces the current input values by a new set. */ - public function replace(array $inputs = []) + public function replace(array $inputs = []): void { $this->parameters = []; $this->add($inputs); @@ -62,7 +53,7 @@ public function replace(array $inputs = []) /** * Adds input values. */ - public function add(array $inputs = []) + public function add(array $inputs = []): void { foreach ($inputs as $input => $value) { $this->set($input, $value); @@ -74,19 +65,44 @@ public function add(array $inputs = []) * * @param string|int|float|bool|array|null $value */ - public function set(string $key, $value) + public function set(string $key, mixed $value): void { - if (null !== $value && !\is_scalar($value) && !\is_array($value) && !method_exists($value, '__toString')) { - trigger_deprecation('symfony/http-foundation', '5.1', 'Passing "%s" as a 2nd Argument to "%s()" is deprecated, pass a scalar, array, or null instead.', get_debug_type($value), __METHOD__); + if (null !== $value && !\is_scalar($value) && !\is_array($value) && !$value instanceof \Stringable) { + throw new \InvalidArgumentException(sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value))); } $this->parameters[$key] = $value; } /** - * {@inheritdoc} + * Returns the parameter value converted to an enum. + * + * @template T of \BackedEnum + * + * @param class-string $class + * @param ?T $default + * + * @return ?T */ - public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = []) + public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum + { + try { + return parent::getEnum($key, $class, $default); + } catch (UnexpectedValueException $e) { + throw new BadRequestException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns the parameter value converted to string. + */ + public function getString(string $key, string $default = ''): string + { + // Shortcuts the parent method because the validation on scalar is already done in get(). + return (string) $this->get($key, $default); + } + + public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { $value = $this->has($key) ? $this->all()[$key] : $default; @@ -96,18 +112,29 @@ public function filter(string $key, $default = null, int $filter = \FILTER_DEFAU } if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { - trigger_deprecation('symfony/http-foundation', '5.1', 'Filtering an array value with "%s()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated', __METHOD__); - - if (!isset($options['flags'])) { - $options['flags'] = \FILTER_REQUIRE_ARRAY; - } + throw new BadRequestException(sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key)); } if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { - trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__); - // throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); } - return filter_var($value, $filter, $options); + $options['flags'] ??= 0; + $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; + $options['flags'] |= \FILTER_NULL_ON_FAILURE; + + $value = filter_var($value, $filter, $options); + + if (null !== $value || $nullOnFailure) { + return $value; + } + + $method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; + $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; + $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; + + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw a "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, BadRequestException::class); + + return false; } } diff --git a/symfony/http-foundation/IpUtils.php b/symfony/http-foundation/IpUtils.php index 49d9a9d74..ceab620c2 100644 --- a/symfony/http-foundation/IpUtils.php +++ b/symfony/http-foundation/IpUtils.php @@ -18,7 +18,22 @@ */ class IpUtils { - private static $checkedIps = []; + public const PRIVATE_SUBNETS = [ + '127.0.0.0/8', // RFC1700 (Loopback) + '10.0.0.0/8', // RFC1918 + '192.168.0.0/16', // RFC1918 + '172.16.0.0/12', // RFC1918 + '169.254.0.0/16', // RFC3927 + '0.0.0.0/8', // RFC5735 + '240.0.0.0/4', // RFC1112 + '::1/128', // Loopback + 'fc00::/7', // Unique Local Address + 'fe80::/10', // Link Local Address + '::ffff:0:0/96', // IPv4 translations + '::/128', // Unspecified address + ]; + + private static array $checkedIps = []; /** * This class should not be instantiated. @@ -31,17 +46,9 @@ private function __construct() * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. * * @param string|array $ips List of IPs or subnets (can be a string if only a single one) - * - * @return bool */ - public static function checkIp(?string $requestIp, $ips) + public static function checkIp(string $requestIp, string|array $ips): bool { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - if (!\is_array($ips)) { $ips = [$ips]; } @@ -65,32 +72,26 @@ public static function checkIp(?string $requestIp, $ips) * * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet */ - public static function checkIp4(?string $requestIp, string $ip) + public static function checkIp4(string $requestIp, string $ip): bool { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - $cacheKey = $requestIp.'-'.$ip.'-v4'; - if (isset(self::$checkedIps[$cacheKey])) { - return self::$checkedIps[$cacheKey]; + if (null !== $cacheValue = self::getCacheResult($cacheKey)) { + return $cacheValue; } if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); if ('0' === $netmask) { - return self::$checkedIps[$cacheKey] = false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + return self::setCacheResult($cacheKey, false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)); } if ($netmask < 0 || $netmask > 32) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } } else { $address = $ip; @@ -98,10 +99,10 @@ public static function checkIp4(?string $requestIp, string $ip) } if (false === ip2long($address)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } - return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + return self::setCacheResult($cacheKey, 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask)); } /** @@ -114,21 +115,13 @@ public static function checkIp4(?string $requestIp, string $ip) * * @param string $ip IPv6 address or subnet in CIDR notation * - * @return bool - * * @throws \RuntimeException When IPV6 support is not enabled */ - public static function checkIp6(?string $requestIp, string $ip) + public static function checkIp6(string $requestIp, string $ip): bool { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - $cacheKey = $requestIp.'-'.$ip.'-v6'; - if (isset(self::$checkedIps[$cacheKey])) { - return self::$checkedIps[$cacheKey]; + if (null !== $cacheValue = self::getCacheResult($cacheKey)) { + return $cacheValue; } if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { @@ -137,14 +130,14 @@ public static function checkIp6(?string $requestIp, string $ip) // Check to see if we were given a IP4 $requestIp or $ip by mistake if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } if ('0' === $netmask) { @@ -152,11 +145,11 @@ public static function checkIp6(?string $requestIp, string $ip) } if ($netmask < 1 || $netmask > 128) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } } else { if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } $address = $ip; @@ -167,7 +160,7 @@ public static function checkIp6(?string $requestIp, string $ip) $bytesTest = unpack('n*', @inet_pton($requestIp)); if (!$bytesAddr || !$bytesTest) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { @@ -175,11 +168,11 @@ public static function checkIp6(?string $requestIp, string $ip) $left = ($left <= 16) ? $left : 16; $mask = ~(0xFFFF >> $left) & 0xFFFF; if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } } - return self::$checkedIps[$cacheKey] = true; + return self::setCacheResult($cacheKey, true); } /** @@ -190,7 +183,7 @@ public static function checkIp6(?string $requestIp, string $ip) public static function anonymize(string $ip): string { $wrappedIPv6 = false; - if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) { + if (str_starts_with($ip, '[') && str_ends_with($ip, ']')) { $wrappedIPv6 = true; $ip = substr($ip, 1, -1); } @@ -213,4 +206,36 @@ public static function anonymize(string $ip): string return $ip; } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of private IP subnets. + */ + public static function isPrivateIp(string $requestIp): bool + { + return self::checkIp($requestIp, self::PRIVATE_SUBNETS); + } + + private static function getCacheResult(string $cacheKey): ?bool + { + if (isset(self::$checkedIps[$cacheKey])) { + // Move the item last in cache (LRU) + $value = self::$checkedIps[$cacheKey]; + unset(self::$checkedIps[$cacheKey]); + self::$checkedIps[$cacheKey] = $value; + + return self::$checkedIps[$cacheKey]; + } + + return null; + } + + private static function setCacheResult(string $cacheKey, bool $result): bool + { + if (1000 < \count(self::$checkedIps)) { + // stop memory leak if there are many keys + self::$checkedIps = \array_slice(self::$checkedIps, 500, null, true); + } + + return self::$checkedIps[$cacheKey] = $result; + } } diff --git a/symfony/http-foundation/JsonResponse.php b/symfony/http-foundation/JsonResponse.php index 501a6387d..93c5751f2 100644 --- a/symfony/http-foundation/JsonResponse.php +++ b/symfony/http-foundation/JsonResponse.php @@ -34,12 +34,9 @@ class JsonResponse extends Response protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS; /** - * @param mixed $data The response data - * @param int $status The response status code - * @param array $headers An array of response headers - * @param bool $json If the data is already a JSON string + * @param bool $json If the data is already a JSON string */ - public function __construct($data = null, int $status = 200, array $headers = [], bool $json = false) + public function __construct(mixed $data = null, int $status = 200, array $headers = [], bool $json = false) { parent::__construct('', $status, $headers); @@ -47,36 +44,11 @@ public function __construct($data = null, int $status = 200, array $headers = [] throw new \TypeError(sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); } - if (null === $data) { - $data = new \ArrayObject(); - } + $data ??= new \ArrayObject(); $json ? $this->setJson($data) : $this->setData($data); } - /** - * Factory method for chainability. - * - * Example: - * - * return JsonResponse::create(['key' => 'value']) - * ->setSharedMaxAge(300); - * - * @param mixed $data The JSON response data - * @param int $status The response status code - * @param array $headers An array of response headers - * - * @return static - * - * @deprecated since Symfony 5.1, use __construct() instead. - */ - public static function create($data = null, int $status = 200, array $headers = []) - { - trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); - - return new static($data, $status, $headers); - } - /** * Factory method for chainability. * @@ -86,12 +58,10 @@ public static function create($data = null, int $status = 200, array $headers = * ->setSharedMaxAge(300); * * @param string $data The JSON response string - * @param int $status The response status code + * @param int $status The response status code (200 "OK" by default) * @param array $headers An array of response headers - * - * @return static */ - public static function fromJsonString(string $data, int $status = 200, array $headers = []) + public static function fromJsonString(string $data, int $status = 200, array $headers = []): static { return new static($data, $status, $headers, true); } @@ -105,8 +75,11 @@ public static function fromJsonString(string $data, int $status = 200, array $he * * @throws \InvalidArgumentException When the callback name is not valid */ - public function setCallback(string $callback = null) + public function setCallback(?string $callback = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if (null !== $callback) { // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ // partially taken from https://github.com/willdurand/JsonpCallbackValidator @@ -136,7 +109,7 @@ public function setCallback(string $callback = null) * * @return $this */ - public function setJson(string $json) + public function setJson(string $json): static { $this->data = $json; @@ -146,24 +119,22 @@ public function setJson(string $json) /** * Sets the data to be sent as JSON. * - * @param mixed $data - * * @return $this * * @throws \InvalidArgumentException */ - public function setData($data = []) + public function setData(mixed $data = []): static { try { $data = json_encode($data, $this->encodingOptions); } catch (\Exception $e) { - if ('Exception' === \get_class($e) && str_starts_with($e->getMessage(), 'Failed calling ')) { + if ('Exception' === $e::class && str_starts_with($e->getMessage(), 'Failed calling ')) { throw $e->getPrevious() ?: $e; } throw $e; } - if (\PHP_VERSION_ID >= 70300 && (\JSON_THROW_ON_ERROR & $this->encodingOptions)) { + if (\JSON_THROW_ON_ERROR & $this->encodingOptions) { return $this->setJson($data); } @@ -176,10 +147,8 @@ public function setData($data = []) /** * Returns options used while encoding data to JSON. - * - * @return int */ - public function getEncodingOptions() + public function getEncodingOptions(): int { return $this->encodingOptions; } @@ -189,7 +158,7 @@ public function getEncodingOptions() * * @return $this */ - public function setEncodingOptions(int $encodingOptions) + public function setEncodingOptions(int $encodingOptions): static { $this->encodingOptions = $encodingOptions; @@ -201,7 +170,7 @@ public function setEncodingOptions(int $encodingOptions) * * @return $this */ - protected function update() + protected function update(): static { if (null !== $this->callback) { // Not using application/javascript for compatibility reasons with older browsers. diff --git a/symfony/http-foundation/ParameterBag.php b/symfony/http-foundation/ParameterBag.php index e1f89d69e..48fa4b233 100644 --- a/symfony/http-foundation/ParameterBag.php +++ b/symfony/http-foundation/ParameterBag.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; /** * ParameterBag is a container for key/value pairs. @@ -36,13 +37,9 @@ public function __construct(array $parameters = []) * Returns the parameters. * * @param string|null $key The name of the parameter to return or null to get them all - * - * @return array */ - public function all(/* string $key = null */) + public function all(?string $key = null): array { - $key = \func_num_args() > 0 ? func_get_arg(0) : null; - if (null === $key) { return $this->parameters; } @@ -56,16 +53,16 @@ public function all(/* string $key = null */) /** * Returns the parameter keys. - * - * @return array */ - public function keys() + public function keys(): array { return array_keys($this->parameters); } /** * Replaces the current parameters by a new set. + * + * @return void */ public function replace(array $parameters = []) { @@ -74,46 +71,39 @@ public function replace(array $parameters = []) /** * Adds parameters. + * + * @return void */ public function add(array $parameters = []) { $this->parameters = array_replace($this->parameters, $parameters); } - /** - * Returns a parameter by name. - * - * @param mixed $default The default value if the parameter key does not exist - * - * @return mixed - */ - public function get(string $key, $default = null) + public function get(string $key, mixed $default = null): mixed { return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; } /** - * Sets a parameter by name. - * - * @param mixed $value The value + * @return void */ - public function set(string $key, $value) + public function set(string $key, mixed $value) { $this->parameters[$key] = $value; } /** * Returns true if the parameter is defined. - * - * @return bool */ - public function has(string $key) + public function has(string $key): bool { return \array_key_exists($key, $this->parameters); } /** * Removes a parameter. + * + * @return void */ public function remove(string $key) { @@ -122,67 +112,92 @@ public function remove(string $key) /** * Returns the alphabetic characters of the parameter value. - * - * @return string */ - public function getAlpha(string $key, string $default = '') + public function getAlpha(string $key, string $default = ''): string { - return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default)); + return preg_replace('/[^[:alpha:]]/', '', $this->getString($key, $default)); } /** * Returns the alphabetic characters and digits of the parameter value. - * - * @return string */ - public function getAlnum(string $key, string $default = '') + public function getAlnum(string $key, string $default = ''): string { - return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default)); + return preg_replace('/[^[:alnum:]]/', '', $this->getString($key, $default)); } /** * Returns the digits of the parameter value. - * - * @return string */ - public function getDigits(string $key, string $default = '') + public function getDigits(string $key, string $default = ''): string { - // we need to remove - and + because they're allowed in the filter - return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT)); + return preg_replace('/[^[:digit:]]/', '', $this->getString($key, $default)); + } + + /** + * Returns the parameter as string. + */ + public function getString(string $key, string $default = ''): string + { + $value = $this->get($key, $default); + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException(sprintf('Parameter value "%s" cannot be converted to "string".', $key)); + } + + return (string) $value; } /** * Returns the parameter value converted to integer. - * - * @return int */ - public function getInt(string $key, int $default = 0) + public function getInt(string $key, int $default = 0): int { - return (int) $this->get($key, $default); + // In 7.0 remove the fallback to 0, in case of failure an exception will be thrown + return $this->filter($key, $default, \FILTER_VALIDATE_INT, ['flags' => \FILTER_REQUIRE_SCALAR]) ?: 0; } /** * Returns the parameter value converted to boolean. + */ + public function getBoolean(string $key, bool $default = false): bool + { + return $this->filter($key, $default, \FILTER_VALIDATE_BOOL, ['flags' => \FILTER_REQUIRE_SCALAR]); + } + + /** + * Returns the parameter value converted to an enum. * - * @return bool + * @template T of \BackedEnum + * + * @param class-string $class + * @param ?T $default + * + * @return ?T */ - public function getBoolean(string $key, bool $default = false) + public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum { - return $this->filter($key, $default, \FILTER_VALIDATE_BOOLEAN); + $value = $this->get($key); + + if (null === $value) { + return $default; + } + + try { + return $class::from($value); + } catch (\ValueError|\TypeError $e) { + throw new UnexpectedValueException(sprintf('Parameter "%s" cannot be converted to enum: %s.', $key, $e->getMessage()), $e->getCode(), $e); + } } /** * Filter key. * - * @param mixed $default Default = null - * @param int $filter FILTER_* constant - * @param mixed $options Filter options + * @param int $filter FILTER_* constant + * @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants * * @see https://php.net/filter-var - * - * @return mixed */ - public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = []) + public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { $value = $this->get($key, $default); @@ -196,12 +211,31 @@ public function filter(string $key, $default = null, int $filter = \FILTER_DEFAU $options['flags'] = \FILTER_REQUIRE_ARRAY; } + if (\is_object($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException(sprintf('Parameter value "%s" cannot be filtered.', $key)); + } + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { - trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__); - // throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); } - return filter_var($value, $filter, $options); + $options['flags'] ??= 0; + $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; + $options['flags'] |= \FILTER_NULL_ON_FAILURE; + + $value = filter_var($value, $filter, $options); + + if (null !== $value || $nullOnFailure) { + return $value; + } + + $method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; + $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; + $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; + + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, UnexpectedValueException::class); + + return false; } /** @@ -209,19 +243,15 @@ public function filter(string $key, $default = null, int $filter = \FILTER_DEFAU * * @return \ArrayIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->parameters); } /** * Returns the number of parameters. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->parameters); } diff --git a/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php index a6dd993b7..550090f97 100644 --- a/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php +++ b/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php @@ -17,14 +17,24 @@ use Symfony\Component\RateLimiter\RateLimit; /** - * An implementation of RequestRateLimiterInterface that + * An implementation of PeekableRequestRateLimiterInterface that * fits most use-cases. * * @author Wouter de Jong */ -abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface +abstract class AbstractRequestRateLimiter implements PeekableRequestRateLimiterInterface { public function consume(Request $request): RateLimit + { + return $this->doConsume($request, 1); + } + + public function peek(Request $request): RateLimit + { + return $this->doConsume($request, 0); + } + + private function doConsume(Request $request, int $tokens): RateLimit { $limiters = $this->getLimiters($request); if (0 === \count($limiters)) { @@ -33,7 +43,7 @@ public function consume(Request $request): RateLimit $minimalRateLimit = null; foreach ($limiters as $limiter) { - $rateLimit = $limiter->consume(1); + $rateLimit = $limiter->consume($tokens); $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; } diff --git a/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php b/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php new file mode 100644 index 000000000..63471af22 --- /dev/null +++ b/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * A request limiter which allows peeking ahead. + * + * This is valuable to reduce the cache backend load in scenarios + * like a login when we only want to consume a token on login failure, + * and where the majority of requests will be successful and thus not + * need to consume a token. + * + * This way we can peek ahead before allowing the request through, and + * only consume if the request failed (1 backend op). This is compared + * to always consuming and then resetting the limit if the request + * is successful (2 backend ops). + * + * @author Jordi Boggiano + */ +interface PeekableRequestRateLimiterInterface extends RequestRateLimiterInterface +{ + public function peek(Request $request): RateLimit; +} diff --git a/symfony/http-foundation/RedirectResponse.php b/symfony/http-foundation/RedirectResponse.php index 2103280c6..408629e36 100644 --- a/symfony/http-foundation/RedirectResponse.php +++ b/symfony/http-foundation/RedirectResponse.php @@ -25,7 +25,7 @@ class RedirectResponse extends Response * * @param string $url The URL to redirect to. The URL should be a full URL, with schema etc., * but practically every browser redirects on paths only as well - * @param int $status The status code (302 by default) + * @param int $status The HTTP status code (302 "Found" by default) * @param array $headers The headers (Location is always set to the given URL) * * @throws \InvalidArgumentException @@ -47,28 +47,10 @@ public function __construct(string $url, int $status = 302, array $headers = []) } } - /** - * Factory method for chainability. - * - * @param string $url The URL to redirect to - * - * @return static - * - * @deprecated since Symfony 5.1, use __construct() instead. - */ - public static function create($url = '', int $status = 302, array $headers = []) - { - trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); - - return new static($url, $status, $headers); - } - /** * Returns the target URL. - * - * @return string */ - public function getTargetUrl() + public function getTargetUrl(): string { return $this->targetUrl; } @@ -80,7 +62,7 @@ public function getTargetUrl() * * @throws \InvalidArgumentException */ - public function setTargetUrl(string $url) + public function setTargetUrl(string $url): static { if ('' === $url) { throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); @@ -103,6 +85,7 @@ public function setTargetUrl(string $url) ', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); $this->headers->set('Location', $url); + $this->headers->set('Content-Type', 'text/html; charset=utf-8'); return $this; } diff --git a/symfony/http-foundation/Request.php b/symfony/http-foundation/Request.php index 28cebad16..fc37c32cc 100644 --- a/symfony/http-foundation/Request.php +++ b/symfony/http-foundation/Request.php @@ -48,8 +48,6 @@ class Request public const HEADER_X_FORWARDED_PORT = 0b010000; public const HEADER_X_FORWARDED_PREFIX = 0b100000; - /** @deprecated since Symfony 5.2, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead. */ - public const HEADER_X_FORWARDED_ALL = 0b1011110; // All "X-Forwarded-*" headers sent by "usual" reverse proxy public const HEADER_X_FORWARDED_AWS_ELB = 0b0011010; // AWS ELB doesn't send X-Forwarded-Host public const HEADER_X_FORWARDED_TRAEFIK = 0b0111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy @@ -91,6 +89,8 @@ class Request /** * Request body parameters ($_POST). * + * @see getPayload() for portability between content types + * * @var InputBag */ public $request; @@ -136,57 +136,57 @@ class Request protected $content; /** - * @var array + * @var string[]|null */ protected $languages; /** - * @var array + * @var string[]|null */ protected $charsets; /** - * @var array + * @var string[]|null */ protected $encodings; /** - * @var array + * @var string[]|null */ protected $acceptableContentTypes; /** - * @var string + * @var string|null */ protected $pathInfo; /** - * @var string + * @var string|null */ protected $requestUri; /** - * @var string + * @var string|null */ protected $baseUrl; /** - * @var string + * @var string|null */ protected $basePath; /** - * @var string + * @var string|null */ protected $method; /** - * @var string + * @var string|null */ protected $format; /** - * @var SessionInterface|callable(): SessionInterface + * @var SessionInterface|callable():SessionInterface|null */ protected $session; @@ -201,25 +201,20 @@ class Request protected $defaultLocale = 'en'; /** - * @var array + * @var array|null */ protected static $formats; protected static $requestFactory; - /** - * @var string|null - */ - private $preferredFormat; - private $isHostValid = true; - private $isForwardedValid = true; + private ?string $preferredFormat = null; + private bool $isHostValid = true; + private bool $isForwardedValid = true; + private bool $isSafeContentPreferred; - /** - * @var bool|null - */ - private $isSafeContentPreferred; + private array $trustedValuesCache = []; - private static $trustedHeaderSet = -1; + private static int $trustedHeaderSet = -1; private const FORWARDED_PARAMS = [ self::HEADER_X_FORWARDED_FOR => 'for', @@ -246,6 +241,9 @@ class Request self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX', ]; + /** @var bool */ + private $isIisRewrite = false; + /** * @param array $query The GET parameters * @param array $request The POST parameters @@ -272,6 +270,8 @@ public function __construct(array $query = [], array $request = [], array $attri * @param array $files The FILES parameters * @param array $server The SERVER parameters * @param string|resource|null $content The raw body data + * + * @return void */ public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { @@ -298,10 +298,8 @@ public function initialize(array $query = [], array $request = [], array $attrib /** * Creates a new request with values from PHP's super globals. - * - * @return static */ - public static function createFromGlobals() + public static function createFromGlobals(): static { $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); @@ -328,10 +326,8 @@ public static function createFromGlobals() * @param array $files The request files ($_FILES) * @param array $server The server parameters ($_SERVER) * @param string|resource|null $content The raw body data - * - * @return static */ - public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null) + public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null): static { $server = array_replace([ 'SERVER_NAME' => 'localhost', @@ -352,7 +348,12 @@ public static function create(string $uri, string $method = 'GET', array $parame $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); - $components = parse_url($uri); + if (false === ($components = parse_url($uri)) && '/' === ($uri[0] ?? '')) { + trigger_deprecation('symfony/http-foundation', '6.3', 'Calling "%s()" with an invalid URI is deprecated.', __METHOD__); + $components = parse_url($uri.'#'); + unset($components['fragment']); + } + if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; $server['HTTP_HOST'] = $components['host']; @@ -430,6 +431,8 @@ public static function create(string $uri, string $method = 'GET', array $parame * This is mainly useful when you need to override the Request class * to keep BC with an existing system. It should not be used for any * other purpose. + * + * @return void */ public static function setFactory(?callable $callable) { @@ -445,10 +448,8 @@ public static function setFactory(?callable $callable) * @param array|null $cookies The COOKIE parameters * @param array|null $files The FILES parameters * @param array|null $server The SERVER parameters - * - * @return static */ - public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) + public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null): static { $dup = clone $this; if (null !== $query) { @@ -509,12 +510,7 @@ public function __clone() $this->headers = clone $this->headers; } - /** - * Returns the request as a string. - * - * @return string - */ - public function __toString() + public function __toString(): string { $content = $this->getContent(); @@ -541,6 +537,8 @@ public function __toString() * * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE. * $_FILES is never overridden, see rfc1867 + * + * @return void */ public function overrideGlobals() { @@ -581,12 +579,11 @@ public function overrideGlobals() * * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies + * + * @return void */ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) { - if (self::HEADER_X_FORWARDED_ALL === $trustedHeaderSet) { - trigger_deprecation('symfony/http-foundation', '5.2', 'The "HEADER_X_FORWARDED_ALL" constant is deprecated, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead.'); - } self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) { if ('REMOTE_ADDR' !== $proxy) { $proxies[] = $proxy; @@ -602,9 +599,9 @@ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) /** * Gets the list of trusted proxies. * - * @return array + * @return string[] */ - public static function getTrustedProxies() + public static function getTrustedProxies(): array { return self::$trustedProxies; } @@ -614,7 +611,7 @@ public static function getTrustedProxies() * * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies */ - public static function getTrustedHeaderSet() + public static function getTrustedHeaderSet(): int { return self::$trustedHeaderSet; } @@ -625,12 +622,12 @@ public static function getTrustedHeaderSet() * You should only list the hosts you manage using regexs. * * @param array $hostPatterns A list of trusted host patterns + * + * @return void */ public static function setTrustedHosts(array $hostPatterns) { - self::$trustedHostPatterns = array_map(function ($hostPattern) { - return sprintf('{%s}i', $hostPattern); - }, $hostPatterns); + self::$trustedHostPatterns = array_map(fn ($hostPattern) => sprintf('{%s}i', $hostPattern), $hostPatterns); // we need to reset trusted hosts on trusted host patterns change self::$trustedHosts = []; } @@ -638,9 +635,9 @@ public static function setTrustedHosts(array $hostPatterns) /** * Gets the list of trusted host patterns. * - * @return array + * @return string[] */ - public static function getTrustedHosts() + public static function getTrustedHosts(): array { return self::$trustedHostPatterns; } @@ -650,10 +647,8 @@ public static function getTrustedHosts() * * It builds a normalized query string, where keys/value pairs are alphabetized, * have consistent escaping and unneeded delimiters are removed. - * - * @return string */ - public static function normalizeQueryString(?string $qs) + public static function normalizeQueryString(?string $qs): string { if ('' === ($qs ?? '')) { return ''; @@ -675,6 +670,8 @@ public static function normalizeQueryString(?string $qs) * If these methods are not protected against CSRF, this presents a possible vulnerability. * * The HTTP method can only be overridden when the real HTTP method is POST. + * + * @return void */ public static function enableHttpMethodParameterOverride() { @@ -683,10 +680,8 @@ public static function enableHttpMethodParameterOverride() /** * Checks whether support for the _method request parameter is enabled. - * - * @return bool */ - public static function getHttpMethodParameterOverride() + public static function getHttpMethodParameterOverride(): bool { return self::$httpMethodParameterOverride; } @@ -700,13 +695,9 @@ public static function getHttpMethodParameterOverride() * * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST * - * @param mixed $default The default value if the parameter key does not exist - * - * @return mixed - * - * @internal since Symfony 5.4, use explicit input sources instead + * @internal use explicit input sources instead */ - public function get(string $key, $default = null) + public function get(string $key, mixed $default = null): mixed { if ($this !== $result = $this->attributes->get($key, $this)) { return $result; @@ -726,9 +717,9 @@ public function get(string $key, $default = null) /** * Gets the Session. * - * @return SessionInterface + * @throws SessionNotFoundException When session is not set properly */ - public function getSession() + public function getSession(): SessionInterface { $session = $this->session; if (!$session instanceof SessionInterface && null !== $session) { @@ -745,10 +736,8 @@ public function getSession() /** * Whether the request contains a Session which was started in one of the * previous requests. - * - * @return bool */ - public function hasPreviousSession() + public function hasPreviousSession(): bool { // the check for $this->session avoids malicious users trying to fake a session cookie with proper name return $this->hasSession() && $this->cookies->has($this->getSession()->getName()); @@ -762,16 +751,15 @@ public function hasPreviousSession() * is associated with a Session instance. * * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory` - * - * @return bool */ - public function hasSession(/* bool $skipIfUninitialized = false */) + public function hasSession(bool $skipIfUninitialized = false): bool { - $skipIfUninitialized = \func_num_args() > 0 ? func_get_arg(0) : false; - return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface); } + /** + * @return void + */ public function setSession(SessionInterface $session) { $this->session = $session; @@ -782,9 +770,9 @@ public function setSession(SessionInterface $session) * * @param callable(): SessionInterface $factory */ - public function setSessionFactory(callable $factory) + public function setSessionFactory(callable $factory): void { - $this->session = $factory; + $this->session = $factory(...); } /** @@ -796,11 +784,9 @@ public function setSessionFactory(callable $factory) * * Use this method carefully; you should use getClientIp() instead. * - * @return array - * * @see getClientIp() */ - public function getClientIps() + public function getClientIps(): array { $ip = $this->server->get('REMOTE_ADDR'); @@ -824,12 +810,10 @@ public function getClientIps() * ("Client-Ip" for instance), configure it via the $trustedHeaderSet * argument of the Request::setTrustedProxies() method instead. * - * @return string|null - * * @see getClientIps() * @see https://wikipedia.org/wiki/X-Forwarded-For */ - public function getClientIp() + public function getClientIp(): ?string { $ipAddresses = $this->getClientIps(); @@ -838,10 +822,8 @@ public function getClientIp() /** * Returns current script name. - * - * @return string */ - public function getScriptName() + public function getScriptName(): string { return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', '')); } @@ -860,13 +842,9 @@ public function getScriptName() * * @return string The raw path (i.e. not urldecoded) */ - public function getPathInfo() + public function getPathInfo(): string { - if (null === $this->pathInfo) { - $this->pathInfo = $this->preparePathInfo(); - } - - return $this->pathInfo; + return $this->pathInfo ??= $this->preparePathInfo(); } /** @@ -881,13 +859,9 @@ public function getPathInfo() * * @return string The raw path (i.e. not urldecoded) */ - public function getBasePath() + public function getBasePath(): string { - if (null === $this->basePath) { - $this->basePath = $this->prepareBasePath(); - } - - return $this->basePath; + return $this->basePath ??= $this->prepareBasePath(); } /** @@ -900,7 +874,7 @@ public function getBasePath() * * @return string The raw URL (i.e. not urldecoded) */ - public function getBaseUrl() + public function getBaseUrl(): string { $trustedPrefix = ''; @@ -920,19 +894,13 @@ public function getBaseUrl() */ private function getBaseUrlReal(): string { - if (null === $this->baseUrl) { - $this->baseUrl = $this->prepareBaseUrl(); - } - - return $this->baseUrl; + return $this->baseUrl ??= $this->prepareBaseUrl(); } /** * Gets the request's scheme. - * - * @return string */ - public function getScheme() + public function getScheme(): string { return $this->isSecure() ? 'https' : 'http'; } @@ -947,7 +915,7 @@ public function getScheme() * * @return int|string|null Can be a string if fetched from the server bag */ - public function getPort() + public function getPort(): int|string|null { if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) { $host = $host[0]; @@ -972,20 +940,16 @@ public function getPort() /** * Returns the user. - * - * @return string|null */ - public function getUser() + public function getUser(): ?string { return $this->headers->get('PHP_AUTH_USER'); } /** * Returns the password. - * - * @return string|null */ - public function getPassword() + public function getPassword(): ?string { return $this->headers->get('PHP_AUTH_PW'); } @@ -995,7 +959,7 @@ public function getPassword() * * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server */ - public function getUserInfo() + public function getUserInfo(): ?string { $userinfo = $this->getUser(); @@ -1011,15 +975,13 @@ public function getUserInfo() * Returns the HTTP host being requested. * * The port name will be appended to the host if it's non-standard. - * - * @return string */ - public function getHttpHost() + public function getHttpHost(): string { $scheme = $this->getScheme(); $port = $this->getPort(); - if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) { + if (('http' === $scheme && 80 == $port) || ('https' === $scheme && 443 == $port)) { return $this->getHost(); } @@ -1031,13 +993,9 @@ public function getHttpHost() * * @return string The raw URI (i.e. not URI decoded) */ - public function getRequestUri() + public function getRequestUri(): string { - if (null === $this->requestUri) { - $this->requestUri = $this->prepareRequestUri(); - } - - return $this->requestUri; + return $this->requestUri ??= $this->prepareRequestUri(); } /** @@ -1045,10 +1003,8 @@ public function getRequestUri() * * If the URL was called with basic authentication, the user * and the password are not added to the generated string. - * - * @return string */ - public function getSchemeAndHttpHost() + public function getSchemeAndHttpHost(): string { return $this->getScheme().'://'.$this->getHttpHost(); } @@ -1056,11 +1012,9 @@ public function getSchemeAndHttpHost() /** * Generates a normalized URI (URL) for the Request. * - * @return string - * * @see getQueryString() */ - public function getUri() + public function getUri(): string { if (null !== $qs = $this->getQueryString()) { $qs = '?'.$qs; @@ -1073,10 +1027,8 @@ public function getUri() * Generates a normalized URI for the given path. * * @param string $path A path to use instead of the current one - * - * @return string */ - public function getUriForPath(string $path) + public function getUriForPath(string $path): string { return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path; } @@ -1095,10 +1047,8 @@ public function getUriForPath(string $path) * - "/a/b/" -> "../" * - "/a/b/c/other" -> "other" * - "/a/x/y" -> "../../x/y" - * - * @return string */ - public function getRelativeUriForPath(string $path) + public function getRelativeUriForPath(string $path): string { // be sure that we are dealing with an absolute path if (!isset($path[0]) || '/' !== $path[0]) { @@ -1139,10 +1089,8 @@ public function getRelativeUriForPath(string $path) * * It builds a normalized query string, where keys/value pairs are alphabetized * and have consistent escaping. - * - * @return string|null */ - public function getQueryString() + public function getQueryString(): ?string { $qs = static::normalizeQueryString($this->server->get('QUERY_STRING')); @@ -1156,10 +1104,8 @@ public function getQueryString() * when trusted proxies were set via "setTrustedProxies()". * * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". - * - * @return bool */ - public function isSecure() + public function isSecure(): bool { if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) { return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true); @@ -1178,11 +1124,9 @@ public function isSecure() * * The "X-Forwarded-Host" header must contain the client host name. * - * @return string - * * @throws SuspiciousOperationException when the host name is invalid or not trusted */ - public function getHost() + public function getHost(): string { if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { $host = $host[0]; @@ -1236,6 +1180,8 @@ public function getHost() /** * Sets the request method. + * + * @return void */ public function setMethod(string $method) { @@ -1254,11 +1200,9 @@ public function setMethod(string $method) * * The method is always an uppercased string. * - * @return string - * * @see getRealMethod() */ - public function getMethod() + public function getMethod(): string { if (null !== $this->method) { return $this->method; @@ -1296,21 +1240,17 @@ public function getMethod() /** * Gets the "real" request method. * - * @return string - * * @see getMethod() */ - public function getRealMethod() + public function getRealMethod(): string { return strtoupper($this->server->get('REQUEST_METHOD', 'GET')); } /** * Gets the mime type associated with the format. - * - * @return string|null */ - public function getMimeType(string $format) + public function getMimeType(string $format): ?string { if (null === static::$formats) { static::initializeFormats(); @@ -1322,9 +1262,9 @@ public function getMimeType(string $format) /** * Gets the mime types associated with the format. * - * @return array + * @return string[] */ - public static function getMimeTypes(string $format) + public static function getMimeTypes(string $format): array { if (null === static::$formats) { static::initializeFormats(); @@ -1335,10 +1275,8 @@ public static function getMimeTypes(string $format) /** * Gets the format associated with the mime type. - * - * @return string|null */ - public function getFormat(?string $mimeType) + public function getFormat(?string $mimeType): ?string { $canonicalMimeType = null; if ($mimeType && false !== $pos = strpos($mimeType, ';')) { @@ -1364,9 +1302,11 @@ public function getFormat(?string $mimeType) /** * Associates a format with mime types. * - * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + * @param string|string[] $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + * + * @return void */ - public function setFormat(?string $format, $mimeTypes) + public function setFormat(?string $format, string|array $mimeTypes) { if (null === static::$formats) { static::initializeFormats(); @@ -1385,20 +1325,18 @@ public function setFormat(?string $format, $mimeTypes) * * $default * * @see getPreferredFormat - * - * @return string|null */ - public function getRequestFormat(?string $default = 'html') + public function getRequestFormat(?string $default = 'html'): ?string { - if (null === $this->format) { - $this->format = $this->attributes->get('_format'); - } + $this->format ??= $this->attributes->get('_format'); return $this->format ?? $default; } /** * Sets the request format. + * + * @return void */ public function setRequestFormat(?string $format) { @@ -1406,17 +1344,31 @@ public function setRequestFormat(?string $format) } /** - * Gets the format associated with the request. + * Gets the usual name of the format associated with the request's media type (provided in the Content-Type header). * - * @return string|null + * @deprecated since Symfony 6.2, use getContentTypeFormat() instead */ - public function getContentType() + public function getContentType(): ?string + { + trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s()" method is deprecated, use "getContentTypeFormat()" instead.', __METHOD__); + + return $this->getContentTypeFormat(); + } + + /** + * Gets the usual name of the format associated with the request's media type (provided in the Content-Type header). + * + * @see Request::$formats + */ + public function getContentTypeFormat(): ?string { return $this->getFormat($this->headers->get('CONTENT_TYPE', '')); } /** * Sets the default locale. + * + * @return void */ public function setDefaultLocale(string $locale) { @@ -1429,16 +1381,16 @@ public function setDefaultLocale(string $locale) /** * Get the default locale. - * - * @return string */ - public function getDefaultLocale() + public function getDefaultLocale(): string { return $this->defaultLocale; } /** * Sets the locale. + * + * @return void */ public function setLocale(string $locale) { @@ -1447,10 +1399,8 @@ public function setLocale(string $locale) /** * Get the locale. - * - * @return string */ - public function getLocale() + public function getLocale(): string { return $this->locale ?? $this->defaultLocale; } @@ -1459,10 +1409,8 @@ public function getLocale() * Checks if the request method is of specified type. * * @param string $method Uppercase request method (GET, POST etc) - * - * @return bool */ - public function isMethod(string $method) + public function isMethod(string $method): bool { return $this->getMethod() === strtoupper($method); } @@ -1471,20 +1419,16 @@ public function isMethod(string $method) * Checks whether or not the method is safe. * * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 - * - * @return bool */ - public function isMethodSafe() + public function isMethodSafe(): bool { return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']); } /** * Checks whether or not the method is idempotent. - * - * @return bool */ - public function isMethodIdempotent() + public function isMethodIdempotent(): bool { return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']); } @@ -1493,10 +1437,8 @@ public function isMethodIdempotent() * Checks whether the method is cacheable or not. * * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 - * - * @return bool */ - public function isMethodCacheable() + public function isMethodCacheable(): bool { return \in_array($this->getMethod(), ['GET', 'HEAD']); } @@ -1509,10 +1451,8 @@ public function isMethodCacheable() * server might be different. This returns the former (from the "Via" header) * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns * the latter (from the "SERVER_PROTOCOL" server parameter). - * - * @return string|null */ - public function getProtocolVersion() + public function getProtocolVersion(): ?string { if ($this->isFromTrustedProxy()) { preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches); @@ -1531,6 +1471,8 @@ public function getProtocolVersion() * @param bool $asResource If true, a resource will be returned * * @return string|resource + * + * @psalm-return ($asResource is true ? resource : string) */ public function getContent(bool $asResource = false) { @@ -1570,29 +1512,53 @@ public function getContent(bool $asResource = false) return $this->content; } + /** + * Gets the decoded form or json request body. + * + * @throws JsonException When the body cannot be decoded to an array + */ + public function getPayload(): InputBag + { + if ($this->request->count()) { + return clone $this->request; + } + + if ('' === $content = $this->getContent()) { + return new InputBag([]); + } + + try { + $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new JsonException('Could not decode request body.', $e->getCode(), $e); + } + + if (!\is_array($content)) { + throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + } + + return new InputBag($content); + } + /** * Gets the request body decoded as array, typically from a JSON payload. * - * @return array + * @see getPayload() for portability between content types * * @throws JsonException When the body cannot be decoded to an array */ - public function toArray() + public function toArray(): array { if ('' === $content = $this->getContent()) { throw new JsonException('Request body is empty.'); } try { - $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0)); + $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); } catch (\JsonException $e) { throw new JsonException('Could not decode request body.', $e->getCode(), $e); } - if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) { - throw new JsonException('Could not decode request body: '.json_last_error_msg(), json_last_error()); - } - if (!\is_array($content)) { throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); } @@ -1602,18 +1568,13 @@ public function toArray() /** * Gets the Etags. - * - * @return array */ - public function getETags() + public function getETags(): array { return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY); } - /** - * @return bool - */ - public function isNoCache() + public function isNoCache(): bool { return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); } @@ -1628,7 +1589,7 @@ public function isNoCache() */ public function getPreferredFormat(?string $default = 'html'): ?string { - if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) { + if ($this->preferredFormat ??= $this->getRequestFormat(null)) { return $this->preferredFormat; } @@ -1645,10 +1606,8 @@ public function getPreferredFormat(?string $default = 'html'): ?string * Returns the preferred language. * * @param string[] $locales An array of ordered available locales - * - * @return string|null */ - public function getPreferredLanguage(array $locales = null) + public function getPreferredLanguage(?array $locales = null): ?string { $preferredLanguages = $this->getLanguages(); @@ -1679,9 +1638,9 @@ public function getPreferredLanguage(array $locales = null) /** * Gets a list of languages acceptable by the client browser ordered in the user browser preferences. * - * @return array + * @return string[] */ - public function getLanguages() + public function getLanguages(): array { if (null !== $this->languages) { return $this->languages; @@ -1720,43 +1679,31 @@ public function getLanguages() /** * Gets a list of charsets acceptable by the client browser in preferable order. * - * @return array + * @return string[] */ - public function getCharsets() + public function getCharsets(): array { - if (null !== $this->charsets) { - return $this->charsets; - } - - return $this->charsets = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); + return $this->charsets ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); } /** * Gets a list of encodings acceptable by the client browser in preferable order. * - * @return array + * @return string[] */ - public function getEncodings() + public function getEncodings(): array { - if (null !== $this->encodings) { - return $this->encodings; - } - - return $this->encodings = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); + return $this->encodings ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); } /** * Gets a list of content types acceptable by the client browser in preferable order. * - * @return array + * @return string[] */ - public function getAcceptableContentTypes() + public function getAcceptableContentTypes(): array { - if (null !== $this->acceptableContentTypes) { - return $this->acceptableContentTypes; - } - - return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); + return $this->acceptableContentTypes ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); } /** @@ -1766,10 +1713,8 @@ public function getAcceptableContentTypes() * It is known to work with common JavaScript frameworks: * * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript - * - * @return bool */ - public function isXmlHttpRequest() + public function isXmlHttpRequest(): bool { return 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); } @@ -1781,7 +1726,7 @@ public function isXmlHttpRequest() */ public function preferSafeContent(): bool { - if (null !== $this->isSafeContentPreferred) { + if (isset($this->isSafeContentPreferred)) { return $this->isSafeContentPreferred; } @@ -1801,15 +1746,17 @@ public function preferSafeContent(): bool * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/) */ + /** + * @return string + */ protected function prepareRequestUri() { $requestUri = ''; - if ('1' == $this->server->get('IIS_WasUrlRewritten') && '' != $this->server->get('UNENCODED_URL')) { + if ($this->isIisRewrite() && '' != $this->server->get('UNENCODED_URL')) { // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem) $requestUri = $this->server->get('UNENCODED_URL'); $this->server->remove('UNENCODED_URL'); - $this->server->remove('IIS_WasUrlRewritten'); } elseif ($this->server->has('REQUEST_URI')) { $requestUri = $this->server->get('REQUEST_URI'); @@ -1848,10 +1795,8 @@ protected function prepareRequestUri() /** * Prepares the base URL. - * - * @return string */ - protected function prepareBaseUrl() + protected function prepareBaseUrl(): string { $filename = basename($this->server->get('SCRIPT_FILENAME', '')); @@ -1917,10 +1862,8 @@ protected function prepareBaseUrl() /** * Prepares the base path. - * - * @return string */ - protected function prepareBasePath() + protected function prepareBasePath(): string { $baseUrl = $this->getBaseUrl(); if (empty($baseUrl)) { @@ -1943,10 +1886,8 @@ protected function prepareBasePath() /** * Prepares the path info. - * - * @return string */ - protected function preparePathInfo() + protected function preparePathInfo(): string { if (null === ($requestUri = $this->getRequestUri())) { return '/'; @@ -1975,6 +1916,8 @@ protected function preparePathInfo() /** * Initializes HTTP request formats. + * + * @return void */ protected static function initializeFormats() { @@ -2002,7 +1945,7 @@ private function setPhpDefaultLocale(string $locale): void if (class_exists(\Locale::class, false)) { \Locale::setDefault($locale); } - } catch (\Exception $e) { + } catch (\Exception) { } } @@ -2012,7 +1955,13 @@ private function setPhpDefaultLocale(string $locale): void */ private function getUrlencodedPrefix(string $string, string $prefix): ?string { - if (!str_starts_with(rawurldecode($string), $prefix)) { + if ($this->isIisRewrite()) { + // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case + // see https://github.com/php/php-src/issues/11981 + if (0 !== stripos(rawurldecode($string), $prefix)) { + return null; + } + } elseif (!str_starts_with(rawurldecode($string), $prefix)) { return null; } @@ -2025,7 +1974,7 @@ private function getUrlencodedPrefix(string $string, string $prefix): ?string return null; } - private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): self + private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): static { if (self::$requestFactory) { $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content); @@ -2045,16 +1994,26 @@ private static function createRequestFromFactory(array $query = [], array $reque * * This can be useful to determine whether or not to trust the * contents of a proxy-specific header. - * - * @return bool */ - public function isFromTrustedProxy() + public function isFromTrustedProxy(): bool { return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies); } - private function getTrustedValues(int $type, string $ip = null): array + /** + * This method is rather heavy because it splits and merges headers, and it's called by many other methods such as + * getPort(), isSecure(), getHost(), getClientIps(), getBaseUrl() etc. Thus, we try to cache the results for + * best performance. + */ + private function getTrustedValues(int $type, ?string $ip = null): array { + $cacheKey = $type."\0".((self::$trustedHeaderSet & $type) ? $this->headers->get(self::TRUSTED_HEADERS[$type]) : ''); + $cacheKey .= "\0".$ip."\0".$this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); + + if (isset($this->trustedValuesCache[$cacheKey])) { + return $this->trustedValuesCache[$cacheKey]; + } + $clientValues = []; $forwardedValues = []; @@ -2067,7 +2026,6 @@ private function getTrustedValues(int $type, string $ip = null): array if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) { $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); $parts = HeaderUtils::split($forwarded, ',;='); - $forwardedValues = []; $param = self::FORWARDED_PARAMS[$type]; foreach ($parts as $subParts) { if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) { @@ -2089,15 +2047,15 @@ private function getTrustedValues(int $type, string $ip = null): array } if ($forwardedValues === $clientValues || !$clientValues) { - return $forwardedValues; + return $this->trustedValuesCache[$cacheKey] = $forwardedValues; } if (!$forwardedValues) { - return $clientValues; + return $this->trustedValuesCache[$cacheKey] = $clientValues; } if (!$this->isForwardedValid) { - return null !== $ip ? ['0.0.0.0', $ip] : []; + return $this->trustedValuesCache[$cacheKey] = null !== $ip ? ['0.0.0.0', $ip] : []; } $this->isForwardedValid = false; @@ -2136,13 +2094,27 @@ private function normalizeAndFilterClientIps(array $clientIps, string $ip): arra unset($clientIps[$key]); // Fallback to this when the client IP falls into the range of trusted proxies - if (null === $firstTrustedIp) { - $firstTrustedIp = $clientIp; - } + $firstTrustedIp ??= $clientIp; } } // Now the IP chain contains only untrusted proxies and the client IP return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; } + + /** + * Is this IIS with UrlRewriteModule? + * + * This method consumes, caches and removed the IIS_WasUrlRewritten env var, + * so we don't inherit it to sub-requests. + */ + private function isIisRewrite(): bool + { + if (1 === $this->server->getInt('IIS_WasUrlRewritten')) { + $this->isIisRewrite = true; + $this->server->remove('IIS_WasUrlRewritten'); + } + + return $this->isIisRewrite; + } } diff --git a/symfony/http-foundation/RequestMatcher.php b/symfony/http-foundation/RequestMatcher.php index f2645f9ae..b3ca3715d 100644 --- a/symfony/http-foundation/RequestMatcher.php +++ b/symfony/http-foundation/RequestMatcher.php @@ -11,54 +11,47 @@ namespace Symfony\Component\HttpFoundation; +trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s" class is deprecated, use "%s" instead.', RequestMatcher::class, ChainRequestMatcher::class); + /** * RequestMatcher compares a pre-defined set of checks against a Request instance. * * @author Fabien Potencier + * + * @deprecated since Symfony 6.2, use ChainRequestMatcher instead */ class RequestMatcher implements RequestMatcherInterface { - /** - * @var string|null - */ - private $path; - - /** - * @var string|null - */ - private $host; - - /** - * @var int|null - */ - private $port; + private ?string $path = null; + private ?string $host = null; + private ?int $port = null; /** * @var string[] */ - private $methods = []; + private array $methods = []; /** * @var string[] */ - private $ips = []; + private array $ips = []; /** - * @var array + * @var string[] */ - private $attributes = []; + private array $attributes = []; /** * @var string[] */ - private $schemes = []; + private array $schemes = []; /** * @param string|string[]|null $methods * @param string|string[]|null $ips * @param string|string[]|null $schemes */ - public function __construct(string $path = null, string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, int $port = null) + public function __construct(?string $path = null, ?string $host = null, string|array|null $methods = null, string|array|null $ips = null, array $attributes = [], string|array|null $schemes = null, ?int $port = null) { $this->matchPath($path); $this->matchHost($host); @@ -76,14 +69,18 @@ public function __construct(string $path = null, string $host = null, $methods = * Adds a check for the HTTP scheme. * * @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes + * + * @return void */ - public function matchScheme($scheme) + public function matchScheme(string|array|null $scheme) { $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : []; } /** * Adds a check for the URL host name. + * + * @return void */ public function matchHost(?string $regexp) { @@ -91,9 +88,11 @@ public function matchHost(?string $regexp) } /** - * Adds a check for the the URL port. + * Adds a check for the URL port. * * @param int|null $port The port number to connect to + * + * @return void */ public function matchPort(?int $port) { @@ -102,6 +101,8 @@ public function matchPort(?int $port) /** * Adds a check for the URL path info. + * + * @return void */ public function matchPath(?string $regexp) { @@ -112,6 +113,8 @@ public function matchPath(?string $regexp) * Adds a check for the client IP. * * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * + * @return void */ public function matchIp(string $ip) { @@ -122,38 +125,39 @@ public function matchIp(string $ip) * Adds a check for the client IP. * * @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * + * @return void */ - public function matchIps($ips) + public function matchIps(string|array|null $ips) { $ips = null !== $ips ? (array) $ips : []; - $this->ips = array_reduce($ips, static function (array $ips, string $ip) { - return array_merge($ips, preg_split('/\s*,\s*/', $ip)); - }, []); + $this->ips = array_reduce($ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); } /** * Adds a check for the HTTP method. * * @param string|string[]|null $method An HTTP method or an array of HTTP methods + * + * @return void */ - public function matchMethod($method) + public function matchMethod(string|array|null $method) { $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : []; } /** * Adds a check for request attribute. + * + * @return void */ public function matchAttribute(string $key, string $regexp) { $this->attributes[$key] = $regexp; } - /** - * {@inheritdoc} - */ - public function matches(Request $request) + public function matches(Request $request): bool { if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) { return false; diff --git a/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php b/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php new file mode 100644 index 000000000..09d6f49dc --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request attributes matches all regular expressions. + * + * @author Fabien Potencier + */ +class AttributesRequestMatcher implements RequestMatcherInterface +{ + /** + * @param array $regexps + */ + public function __construct(private array $regexps) + { + } + + public function matches(Request $request): bool + { + foreach ($this->regexps as $key => $regexp) { + $attribute = $request->attributes->get($key); + if (!\is_string($attribute)) { + return false; + } + if (!preg_match('{'.$regexp.'}', $attribute)) { + return false; + } + } + + return true; + } +} diff --git a/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php b/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php new file mode 100644 index 000000000..935853f14 --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * ExpressionRequestMatcher uses an expression to match a Request. + * + * @author Fabien Potencier + */ +class ExpressionRequestMatcher implements RequestMatcherInterface +{ + public function __construct( + private ExpressionLanguage $language, + private Expression|string $expression, + ) { + } + + public function matches(Request $request): bool + { + return $this->language->evaluate($this->expression, [ + 'request' => $request, + 'method' => $request->getMethod(), + 'path' => rawurldecode($request->getPathInfo()), + 'host' => $request->getHost(), + 'ip' => $request->getClientIp(), + 'attributes' => $request->attributes->all(), + ]); + } +} diff --git a/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php b/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php new file mode 100644 index 000000000..2836759cd --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request URL host name matches a regular expression. + * + * @author Fabien Potencier + */ +class HostRequestMatcher implements RequestMatcherInterface +{ + public function __construct(private string $regexp) + { + } + + public function matches(Request $request): bool + { + return preg_match('{'.$this->regexp.'}i', $request->getHost()); + } +} diff --git a/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php b/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php new file mode 100644 index 000000000..333612e2f --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the client IP of a Request. + * + * @author Fabien Potencier + */ +class IpsRequestMatcher implements RequestMatcherInterface +{ + private array $ips; + + /** + * @param string[]|string $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * Strings can contain a comma-delimited list of IPs/ranges + */ + public function __construct(array|string $ips) + { + $this->ips = array_reduce((array) $ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); + } + + public function matches(Request $request): bool + { + if (!$this->ips) { + return true; + } + + return IpUtils::checkIp($request->getClientIp() ?? '', $this->ips); + } +} diff --git a/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php b/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php new file mode 100644 index 000000000..875f992be --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request content is valid JSON. + * + * @author Fabien Potencier + */ +class IsJsonRequestMatcher implements RequestMatcherInterface +{ + public function matches(Request $request): bool + { + return json_validate($request->getContent()); + } +} diff --git a/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php b/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php new file mode 100644 index 000000000..b37f6e3c8 --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the HTTP method of a Request. + * + * @author Fabien Potencier + */ +class MethodRequestMatcher implements RequestMatcherInterface +{ + /** + * @var string[] + */ + private array $methods = []; + + /** + * @param string[]|string $methods An HTTP method or an array of HTTP methods + * Strings can contain a comma-delimited list of methods + */ + public function __construct(array|string $methods) + { + $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static fn (array $methods, string $method) => array_merge($methods, preg_split('/\s*,\s*/', $method)), []); + } + + public function matches(Request $request): bool + { + if (!$this->methods) { + return true; + } + + return \in_array($request->getMethod(), $this->methods, true); + } +} diff --git a/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php b/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php new file mode 100644 index 000000000..c7c7a02c1 --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request URL path info matches a regular expression. + * + * @author Fabien Potencier + */ +class PathRequestMatcher implements RequestMatcherInterface +{ + public function __construct(private string $regexp) + { + } + + public function matches(Request $request): bool + { + return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo())); + } +} diff --git a/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php b/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php new file mode 100644 index 000000000..5a01ce959 --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the HTTP port of a Request. + * + * @author Fabien Potencier + */ +class PortRequestMatcher implements RequestMatcherInterface +{ + public function __construct(private int $port) + { + } + + public function matches(Request $request): bool + { + return $request->getPort() === $this->port; + } +} diff --git a/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php b/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php new file mode 100644 index 000000000..9c9cd58b9 --- /dev/null +++ b/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the HTTP scheme of a Request. + * + * @author Fabien Potencier + */ +class SchemeRequestMatcher implements RequestMatcherInterface +{ + /** + * @var string[] + */ + private array $schemes; + + /** + * @param string[]|string $schemes A scheme or a list of schemes + * Strings can contain a comma-delimited list of schemes + */ + public function __construct(array|string $schemes) + { + $this->schemes = array_reduce(array_map('strtolower', (array) $schemes), static fn (array $schemes, string $scheme) => array_merge($schemes, preg_split('/\s*,\s*/', $scheme)), []); + } + + public function matches(Request $request): bool + { + if (!$this->schemes) { + return true; + } + + return \in_array($request->getScheme(), $this->schemes, true); + } +} diff --git a/symfony/http-foundation/RequestMatcherInterface.php b/symfony/http-foundation/RequestMatcherInterface.php index c2e147858..6dcc3e0fc 100644 --- a/symfony/http-foundation/RequestMatcherInterface.php +++ b/symfony/http-foundation/RequestMatcherInterface.php @@ -20,8 +20,6 @@ interface RequestMatcherInterface { /** * Decides whether the rule(s) implemented by the strategy matches the supplied request. - * - * @return bool */ - public function matches(Request $request); + public function matches(Request $request): bool; } diff --git a/symfony/http-foundation/RequestStack.php b/symfony/http-foundation/RequestStack.php index 855b51816..5aa8ba793 100644 --- a/symfony/http-foundation/RequestStack.php +++ b/symfony/http-foundation/RequestStack.php @@ -24,13 +24,15 @@ class RequestStack /** * @var Request[] */ - private $requests = []; + private array $requests = []; /** * Pushes a Request on the stack. * * This method should generally not be called directly as the stack * management should be taken care of by the application itself. + * + * @return void */ public function push(Request $request) { @@ -44,10 +46,8 @@ public function push(Request $request) * * This method should generally not be called directly as the stack * management should be taken care of by the application itself. - * - * @return Request|null */ - public function pop() + public function pop(): ?Request { if (!$this->requests) { return null; @@ -56,10 +56,7 @@ public function pop() return array_pop($this->requests); } - /** - * @return Request|null - */ - public function getCurrentRequest() + public function getCurrentRequest(): ?Request { return end($this->requests) ?: null; } @@ -80,20 +77,6 @@ public function getMainRequest(): ?Request return $this->requests[0]; } - /** - * Gets the master request. - * - * @return Request|null - * - * @deprecated since symfony/http-foundation 5.3, use getMainRequest() instead - */ - public function getMasterRequest() - { - trigger_deprecation('symfony/http-foundation', '5.3', '"%s()" is deprecated, use "getMainRequest()" instead.', __METHOD__); - - return $this->getMainRequest(); - } - /** * Returns the parent request of the current. * @@ -102,10 +85,8 @@ public function getMasterRequest() * like ESI support. * * If current Request is the main request, it returns null. - * - * @return Request|null */ - public function getParentRequest() + public function getParentRequest(): ?Request { $pos = \count($this->requests) - 2; diff --git a/symfony/http-foundation/Response.php b/symfony/http-foundation/Response.php index 59e974d62..a43e7a9ac 100644 --- a/symfony/http-foundation/Response.php +++ b/symfony/http-foundation/Response.php @@ -98,6 +98,8 @@ class Response 'proxy_revalidate' => false, 'max_age' => true, 's_maxage' => true, + 'stale_if_error' => true, // RFC5861 + 'stale_while_revalidate' => true, // RFC5861 'immutable' => false, 'last_modified' => true, 'etag' => true, @@ -210,6 +212,13 @@ class Response ]; /** + * Tracks headers already sent in informational responses. + */ + private array $sentHeaders; + + /** + * @param int $status The HTTP status code (200 "OK" by default) + * * @throws \InvalidArgumentException When the HTTP status code is not valid */ public function __construct(?string $content = '', int $status = 200, array $headers = []) @@ -220,25 +229,6 @@ public function __construct(?string $content = '', int $status = 200, array $hea $this->setProtocolVersion('1.0'); } - /** - * Factory method for chainability. - * - * Example: - * - * return Response::create($body, 200) - * ->setSharedMaxAge(300); - * - * @return static - * - * @deprecated since Symfony 5.1, use __construct() instead. - */ - public static function create(?string $content = '', int $status = 200, array $headers = []) - { - trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); - - return new static($content, $status, $headers); - } - /** * Returns the Response as an HTTP string. * @@ -246,11 +236,9 @@ public static function create(?string $content = '', int $status = 200, array $h * one that will be sent to the client only if the prepare() method * has been called before. * - * @return string - * * @see prepare() */ - public function __toString() + public function __toString(): string { return sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". @@ -275,7 +263,7 @@ public function __clone() * * @return $this */ - public function prepare(Request $request) + public function prepare(Request $request): static { $headers = $this->headers; @@ -298,7 +286,7 @@ public function prepare(Request $request) $charset = $this->charset ?: 'UTF-8'; if (!$headers->has('Content-Type')) { $headers->set('Content-Type', 'text/html; charset='.$charset); - } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) { + } elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) { // add the charset $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset); } @@ -343,21 +331,52 @@ public function prepare(Request $request) /** * Sends HTTP headers. * + * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null + * * @return $this */ - public function sendHeaders() + public function sendHeaders(/* int $statusCode = null */): static { // headers have already been sent by the developer if (headers_sent()) { return $this; } + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + $informationalResponse = $statusCode >= 100 && $statusCode < 200; + if ($informationalResponse && !\function_exists('headers_send')) { + // skip informational responses if not supported by the SAPI + return $this; + } + // headers foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { + $newValues = $values; + $replace = false; + + // As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed + $previousValues = $this->sentHeaders[$name] ?? null; + if ($previousValues === $values) { + // Header already sent in a previous response, it will be automatically copied in this response by PHP + continue; + } + $replace = 0 === strcasecmp($name, 'Content-Type'); - foreach ($values as $value) { + + if (null !== $previousValues && array_diff($previousValues, $values)) { + header_remove($name); + $previousValues = null; + } + + $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + + foreach ($newValues as $value) { header($name.': '.$value, $replace, $this->statusCode); } + + if ($informationalResponse) { + $this->sentHeaders[$name] = $values; + } } // cookies @@ -365,8 +384,16 @@ public function sendHeaders() header('Set-Cookie: '.$cookie, false, $this->statusCode); } + if ($informationalResponse) { + headers_send($statusCode); + + return $this; + } + + $statusCode ??= $this->statusCode; + // status - header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); + header(sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); return $this; } @@ -376,7 +403,7 @@ public function sendHeaders() * * @return $this */ - public function sendContent() + public function sendContent(): static { echo $this->content; @@ -386,18 +413,25 @@ public function sendContent() /** * Sends HTTP headers and content. * + * @param bool $flush Whether output buffers should be flushed + * * @return $this */ - public function send() + public function send(/* bool $flush = true */): static { $this->sendHeaders(); $this->sendContent(); + $flush = 1 <= \func_num_args() ? func_get_arg(0) : true; + if (!$flush) { + return $this; + } + if (\function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } elseif (\function_exists('litespeed_finish_request')) { litespeed_finish_request(); - } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { static::closeOutputBuffers(0, true); flush(); } @@ -410,7 +444,7 @@ public function send() * * @return $this */ - public function setContent(?string $content) + public function setContent(?string $content): static { $this->content = $content ?? ''; @@ -419,10 +453,8 @@ public function setContent(?string $content) /** * Gets the current response content. - * - * @return string|false */ - public function getContent() + public function getContent(): string|false { return $this->content; } @@ -434,7 +466,7 @@ public function getContent() * * @final */ - public function setProtocolVersion(string $version): object + public function setProtocolVersion(string $version): static { $this->version = $version; @@ -463,7 +495,7 @@ public function getProtocolVersion(): string * * @final */ - public function setStatusCode(int $code, string $text = null): object + public function setStatusCode(int $code, ?string $text = null): static { $this->statusCode = $code; if ($this->isInvalid()) { @@ -476,12 +508,6 @@ public function setStatusCode(int $code, string $text = null): object return $this; } - if (false === $text) { - $this->statusText = ''; - - return $this; - } - $this->statusText = $text; return $this; @@ -504,7 +530,7 @@ public function getStatusCode(): int * * @final */ - public function setCharset(string $charset): object + public function setCharset(string $charset): static { $this->charset = $charset; @@ -585,7 +611,7 @@ public function isValidateable(): bool * * @final */ - public function setPrivate(): object + public function setPrivate(): static { $this->headers->removeCacheControlDirective('public'); $this->headers->addCacheControlDirective('private'); @@ -602,7 +628,7 @@ public function setPrivate(): object * * @final */ - public function setPublic(): object + public function setPublic(): static { $this->headers->addCacheControlDirective('public'); $this->headers->removeCacheControlDirective('private'); @@ -617,7 +643,7 @@ public function setPublic(): object * * @final */ - public function setImmutable(bool $immutable = true): object + public function setImmutable(bool $immutable = true): static { if ($immutable) { $this->headers->addCacheControlDirective('immutable'); @@ -660,7 +686,7 @@ public function mustRevalidate(): bool * * @final */ - public function getDate(): ?\DateTimeInterface + public function getDate(): ?\DateTimeImmutable { return $this->headers->getDate('Date'); } @@ -672,12 +698,9 @@ public function getDate(): ?\DateTimeInterface * * @final */ - public function setDate(\DateTimeInterface $date): object + public function setDate(\DateTimeInterface $date): static { - if ($date instanceof \DateTime) { - $date = \DateTimeImmutable::createFromMutable($date); - } - + $date = \DateTimeImmutable::createFromInterface($date); $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); @@ -703,7 +726,7 @@ public function getAge(): int * * @return $this */ - public function expire() + public function expire(): static { if ($this->isFresh()) { $this->headers->set('Age', $this->getMaxAge()); @@ -718,13 +741,13 @@ public function expire() * * @final */ - public function getExpires(): ?\DateTimeInterface + public function getExpires(): ?\DateTimeImmutable { try { return $this->headers->getDate('Expires'); - } catch (\RuntimeException $e) { + } catch (\RuntimeException) { // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past - return \DateTime::createFromFormat('U', time() - 172800); + return \DateTimeImmutable::createFromFormat('U', time() - 172800); } } @@ -737,18 +760,18 @@ public function getExpires(): ?\DateTimeInterface * * @final */ - public function setExpires(\DateTimeInterface $date = null): object + public function setExpires(?\DateTimeInterface $date = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if (null === $date) { $this->headers->remove('Expires'); return $this; } - if ($date instanceof \DateTime) { - $date = \DateTimeImmutable::createFromMutable($date); - } - + $date = \DateTimeImmutable::createFromInterface($date); $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); @@ -792,13 +815,45 @@ public function getMaxAge(): ?int * * @final */ - public function setMaxAge(int $value): object + public function setMaxAge(int $value): static { $this->headers->addCacheControlDirective('max-age', $value); return $this; } + /** + * Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down. + * + * This method sets the Cache-Control stale-if-error directive. + * + * @return $this + * + * @final + */ + public function setStaleIfError(int $value): static + { + $this->headers->addCacheControlDirective('stale-if-error', $value); + + return $this; + } + + /** + * Sets the number of seconds after which the response should no longer return stale content by shared caches. + * + * This method sets the Cache-Control stale-while-revalidate directive. + * + * @return $this + * + * @final + */ + public function setStaleWhileRevalidate(int $value): static + { + $this->headers->addCacheControlDirective('stale-while-revalidate', $value); + + return $this; + } + /** * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. * @@ -808,7 +863,7 @@ public function setMaxAge(int $value): object * * @final */ - public function setSharedMaxAge(int $value): object + public function setSharedMaxAge(int $value): static { $this->setPublic(); $this->headers->addCacheControlDirective('s-maxage', $value); @@ -842,7 +897,7 @@ public function getTtl(): ?int * * @final */ - public function setTtl(int $seconds): object + public function setTtl(int $seconds): static { $this->setSharedMaxAge($this->getAge() + $seconds); @@ -858,7 +913,7 @@ public function setTtl(int $seconds): object * * @final */ - public function setClientTtl(int $seconds): object + public function setClientTtl(int $seconds): static { $this->setMaxAge($this->getAge() + $seconds); @@ -872,7 +927,7 @@ public function setClientTtl(int $seconds): object * * @final */ - public function getLastModified(): ?\DateTimeInterface + public function getLastModified(): ?\DateTimeImmutable { return $this->headers->getDate('Last-Modified'); } @@ -886,18 +941,18 @@ public function getLastModified(): ?\DateTimeInterface * * @final */ - public function setLastModified(\DateTimeInterface $date = null): object + public function setLastModified(?\DateTimeInterface $date = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if (null === $date) { $this->headers->remove('Last-Modified'); return $this; } - if ($date instanceof \DateTime) { - $date = \DateTimeImmutable::createFromMutable($date); - } - + $date = \DateTimeImmutable::createFromInterface($date); $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); @@ -924,8 +979,11 @@ public function getEtag(): ?string * * @final */ - public function setEtag(string $etag = null, bool $weak = false): object + public function setEtag(?string $etag = null, bool $weak = false): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if (null === $etag) { $this->headers->remove('Etag'); } else { @@ -950,7 +1008,7 @@ public function setEtag(string $etag = null, bool $weak = false): object * * @final */ - public function setCache(array $options): object + public function setCache(array $options): static { if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) { throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); @@ -972,6 +1030,14 @@ public function setCache(array $options): object $this->setSharedMaxAge($options['s_maxage']); } + if (isset($options['stale_while_revalidate'])) { + $this->setStaleWhileRevalidate($options['stale_while_revalidate']); + } + + if (isset($options['stale_if_error'])) { + $this->setStaleIfError($options['stale_if_error']); + } + foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) { if (!$hasValue && isset($options[$directive])) { if ($options[$directive]) { @@ -1013,7 +1079,7 @@ public function setCache(array $options): object * * @final */ - public function setNotModified(): object + public function setNotModified(): static { $this->setStatusCode(304); $this->setContent(null); @@ -1058,14 +1124,13 @@ public function getVary(): array /** * Sets the Vary header. * - * @param string|array $headers - * @param bool $replace Whether to replace the actual value or not (true by default) + * @param bool $replace Whether to replace the actual value or not (true by default) * * @return $this * * @final */ - public function setVary($headers, bool $replace = true): object + public function setVary(string|array $headers, bool $replace = true): static { $this->headers->set('Vary', $headers, $replace); @@ -1217,7 +1282,7 @@ public function isNotFound(): bool * * @final */ - public function isRedirect(string $location = null): bool + public function isRedirect(?string $location = null): bool { return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location')); } diff --git a/symfony/http-foundation/ResponseHeaderBag.php b/symfony/http-foundation/ResponseHeaderBag.php index 1df13fa21..376357d01 100644 --- a/symfony/http-foundation/ResponseHeaderBag.php +++ b/symfony/http-foundation/ResponseHeaderBag.php @@ -44,10 +44,8 @@ public function __construct(array $headers = []) /** * Returns the headers, with original capitalizations. - * - * @return array */ - public function allPreserveCase() + public function allPreserveCase(): array { $headers = []; foreach ($this->all() as $name => $value) { @@ -57,6 +55,9 @@ public function allPreserveCase() return $headers; } + /** + * @return array + */ public function allPreserveCaseWithoutCookies() { $headers = $this->allPreserveCase(); @@ -68,7 +69,7 @@ public function allPreserveCaseWithoutCookies() } /** - * {@inheritdoc} + * @return void */ public function replace(array $headers = []) { @@ -85,10 +86,7 @@ public function replace(array $headers = []) } } - /** - * {@inheritdoc} - */ - public function all(string $key = null) + public function all(?string $key = null): array { $headers = parent::all(); @@ -106,9 +104,9 @@ public function all(string $key = null) } /** - * {@inheritdoc} + * @return void */ - public function set(string $key, $values, bool $replace = true) + public function set(string $key, string|array|null $values, bool $replace = true) { $uniqueKey = strtr($key, self::UPPER, self::LOWER); @@ -137,7 +135,7 @@ public function set(string $key, $values, bool $replace = true) } /** - * {@inheritdoc} + * @return void */ public function remove(string $key) { @@ -161,22 +159,19 @@ public function remove(string $key) } } - /** - * {@inheritdoc} - */ - public function hasCacheControlDirective(string $key) + public function hasCacheControlDirective(string $key): bool { return \array_key_exists($key, $this->computedCacheControl); } - /** - * {@inheritdoc} - */ - public function getCacheControlDirective(string $key) + public function getCacheControlDirective(string $key): bool|string|null { return $this->computedCacheControl[$key] ?? null; } + /** + * @return void + */ public function setCookie(Cookie $cookie) { $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; @@ -185,12 +180,12 @@ public function setCookie(Cookie $cookie) /** * Removes a cookie from the array, but does not unset it in the browser. + * + * @return void */ - public function removeCookie(string $name, ?string $path = '/', string $domain = null) + public function removeCookie(string $name, ?string $path = '/', ?string $domain = null) { - if (null === $path) { - $path = '/'; - } + $path ??= '/'; unset($this->cookies[$domain][$path][$name]); @@ -214,7 +209,7 @@ public function removeCookie(string $name, ?string $path = '/', string $domain = * * @throws \InvalidArgumentException When the $format is invalid */ - public function getCookies(string $format = self::COOKIES_FLAT) + public function getCookies(string $format = self::COOKIES_FLAT): array { if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); @@ -238,14 +233,22 @@ public function getCookies(string $format = self::COOKIES_FLAT) /** * Clears a cookie in the browser. + * + * @param bool $partitioned + * + * @return void */ - public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null) + public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */) { - $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); + $partitioned = 6 < \func_num_args() ? \func_get_arg(6) : false; + + $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite, $partitioned)); } /** * @see HeaderUtils::makeDisposition() + * + * @return string */ public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '') { @@ -257,10 +260,8 @@ public function makeDisposition(string $disposition, string $filename, string $f * * This considers several other headers and calculates or modifies the * cache-control header to a sensible, conservative value. - * - * @return string */ - protected function computeCacheControlValue() + protected function computeCacheControlValue(): string { if (!$this->cacheControl) { if ($this->has('Last-Modified') || $this->has('Expires')) { diff --git a/symfony/http-foundation/ServerBag.php b/symfony/http-foundation/ServerBag.php index 25688d523..09fc38664 100644 --- a/symfony/http-foundation/ServerBag.php +++ b/symfony/http-foundation/ServerBag.php @@ -22,16 +22,14 @@ class ServerBag extends ParameterBag { /** * Gets the HTTP headers. - * - * @return array */ - public function getHeaders() + public function getHeaders(): array { $headers = []; foreach ($this->parameters as $key => $value) { if (str_starts_with($key, 'HTTP_')) { $headers[substr($key, 5)] = $value; - } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { + } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true) && '' !== $value) { $headers[$key] = $value; } } @@ -51,7 +49,7 @@ public function getHeaders() * RewriteCond %{HTTP:Authorization} .+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] * RewriteCond %{REQUEST_FILENAME} !-f - * RewriteRule ^(.*)$ app.php [QSA,L] + * RewriteRule ^(.*)$ index.php [QSA,L] */ $authorizationHeader = null; diff --git a/symfony/http-foundation/Session/Attribute/AttributeBag.php b/symfony/http-foundation/Session/Attribute/AttributeBag.php index f4f051c7a..ad5a6590a 100644 --- a/symfony/http-foundation/Session/Attribute/AttributeBag.php +++ b/symfony/http-foundation/Session/Attribute/AttributeBag.php @@ -18,8 +18,8 @@ */ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable { - private $name = 'attributes'; - private $storageKey; + private string $name = 'attributes'; + private string $storageKey; protected $attributes = []; @@ -31,69 +31,57 @@ public function __construct(string $storageKey = '_sf2_attributes') $this->storageKey = $storageKey; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } /** - * {@inheritdoc} + * @return void */ public function initialize(array &$attributes) { $this->attributes = &$attributes; } - /** - * {@inheritdoc} - */ - public function getStorageKey() + public function getStorageKey(): string { return $this->storageKey; } - /** - * {@inheritdoc} - */ - public function has(string $name) + public function has(string $name): bool { return \array_key_exists($name, $this->attributes); } - /** - * {@inheritdoc} - */ - public function get(string $name, $default = null) + public function get(string $name, mixed $default = null): mixed { return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } /** - * {@inheritdoc} + * @return void */ - public function set(string $name, $value) + public function set(string $name, mixed $value) { $this->attributes[$name] = $value; } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { return $this->attributes; } /** - * {@inheritdoc} + * @return void */ public function replace(array $attributes) { @@ -103,10 +91,7 @@ public function replace(array $attributes) } } - /** - * {@inheritdoc} - */ - public function remove(string $name) + public function remove(string $name): mixed { $retval = null; if (\array_key_exists($name, $this->attributes)) { @@ -117,10 +102,7 @@ public function remove(string $name) return $retval; } - /** - * {@inheritdoc} - */ - public function clear() + public function clear(): mixed { $return = $this->attributes; $this->attributes = []; @@ -133,19 +115,15 @@ public function clear() * * @return \ArrayIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->attributes); } /** * Returns the number of attributes. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->attributes); } diff --git a/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php b/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php index cb5069681..e8cd0b5a4 100644 --- a/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php +++ b/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php @@ -22,34 +22,31 @@ interface AttributeBagInterface extends SessionBagInterface { /** * Checks if an attribute is defined. - * - * @return bool */ - public function has(string $name); + public function has(string $name): bool; /** * Returns an attribute. - * - * @param mixed $default The default value if not found - * - * @return mixed */ - public function get(string $name, $default = null); + public function get(string $name, mixed $default = null): mixed; /** * Sets an attribute. * - * @param mixed $value + * @return void */ - public function set(string $name, $value); + public function set(string $name, mixed $value); /** * Returns attributes. * * @return array */ - public function all(); + public function all(): array; + /** + * @return void + */ public function replace(array $attributes); /** @@ -57,5 +54,5 @@ public function replace(array $attributes); * * @return mixed The removed value or null when it does not exist */ - public function remove(string $name); + public function remove(string $name): mixed; } diff --git a/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php b/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php deleted file mode 100644 index 864b35fb7..000000000 --- a/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php +++ /dev/null @@ -1,161 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpFoundation\Session\Attribute; - -trigger_deprecation('symfony/http-foundation', '5.3', 'The "%s" class is deprecated.', NamespacedAttributeBag::class); - -/** - * This class provides structured storage of session attributes using - * a name spacing character in the key. - * - * @author Drak - * - * @deprecated since Symfony 5.3 - */ -class NamespacedAttributeBag extends AttributeBag -{ - private $namespaceCharacter; - - /** - * @param string $storageKey Session storage key - * @param string $namespaceCharacter Namespace character to use in keys - */ - public function __construct(string $storageKey = '_sf2_attributes', string $namespaceCharacter = '/') - { - $this->namespaceCharacter = $namespaceCharacter; - parent::__construct($storageKey); - } - - /** - * {@inheritdoc} - */ - public function has(string $name) - { - // reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is - $attributes = $this->resolveAttributePath($name); - $name = $this->resolveKey($name); - - if (null === $attributes) { - return false; - } - - return \array_key_exists($name, $attributes); - } - - /** - * {@inheritdoc} - */ - public function get(string $name, $default = null) - { - // reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is - $attributes = $this->resolveAttributePath($name); - $name = $this->resolveKey($name); - - if (null === $attributes) { - return $default; - } - - return \array_key_exists($name, $attributes) ? $attributes[$name] : $default; - } - - /** - * {@inheritdoc} - */ - public function set(string $name, $value) - { - $attributes = &$this->resolveAttributePath($name, true); - $name = $this->resolveKey($name); - $attributes[$name] = $value; - } - - /** - * {@inheritdoc} - */ - public function remove(string $name) - { - $retval = null; - $attributes = &$this->resolveAttributePath($name); - $name = $this->resolveKey($name); - if (null !== $attributes && \array_key_exists($name, $attributes)) { - $retval = $attributes[$name]; - unset($attributes[$name]); - } - - return $retval; - } - - /** - * Resolves a path in attributes property and returns it as a reference. - * - * This method allows structured namespacing of session attributes. - * - * @param string $name Key name - * @param bool $writeContext Write context, default false - * - * @return array|null - */ - protected function &resolveAttributePath(string $name, bool $writeContext = false) - { - $array = &$this->attributes; - $name = (str_starts_with($name, $this->namespaceCharacter)) ? substr($name, 1) : $name; - - // Check if there is anything to do, else return - if (!$name) { - return $array; - } - - $parts = explode($this->namespaceCharacter, $name); - if (\count($parts) < 2) { - if (!$writeContext) { - return $array; - } - - $array[$parts[0]] = []; - - return $array; - } - - unset($parts[\count($parts) - 1]); - - foreach ($parts as $part) { - if (null !== $array && !\array_key_exists($part, $array)) { - if (!$writeContext) { - $null = null; - - return $null; - } - - $array[$part] = []; - } - - $array = &$array[$part]; - } - - return $array; - } - - /** - * Resolves the key from the name. - * - * This is the last part in a dot separated string. - * - * @return string - */ - protected function resolveKey(string $name) - { - if (false !== $pos = strrpos($name, $this->namespaceCharacter)) { - $name = substr($name, $pos + 1); - } - - return $name; - } -} diff --git a/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php b/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php index 8aab3a122..80bbeda0f 100644 --- a/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php +++ b/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php @@ -18,9 +18,9 @@ */ class AutoExpireFlashBag implements FlashBagInterface { - private $name = 'flashes'; - private $flashes = ['display' => [], 'new' => []]; - private $storageKey; + private string $name = 'flashes'; + private array $flashes = ['display' => [], 'new' => []]; + private string $storageKey; /** * @param string $storageKey The key used to store flashes in the session @@ -30,21 +30,21 @@ public function __construct(string $storageKey = '_symfony_flashes') $this->storageKey = $storageKey; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } /** - * {@inheritdoc} + * @return void */ public function initialize(array &$flashes) { @@ -58,33 +58,24 @@ public function initialize(array &$flashes) } /** - * {@inheritdoc} + * @return void */ - public function add(string $type, $message) + public function add(string $type, mixed $message) { $this->flashes['new'][$type][] = $message; } - /** - * {@inheritdoc} - */ - public function peek(string $type, array $default = []) + public function peek(string $type, array $default = []): array { return $this->has($type) ? $this->flashes['display'][$type] : $default; } - /** - * {@inheritdoc} - */ - public function peekAll() + public function peekAll(): array { return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : []; } - /** - * {@inheritdoc} - */ - public function get(string $type, array $default = []) + public function get(string $type, array $default = []): array { $return = $default; @@ -100,10 +91,7 @@ public function get(string $type, array $default = []) return $return; } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { $return = $this->flashes['display']; $this->flashes['display'] = []; @@ -112,7 +100,7 @@ public function all() } /** - * {@inheritdoc} + * @return void */ public function setAll(array $messages) { @@ -120,41 +108,29 @@ public function setAll(array $messages) } /** - * {@inheritdoc} + * @return void */ - public function set(string $type, $messages) + public function set(string $type, string|array $messages) { $this->flashes['new'][$type] = (array) $messages; } - /** - * {@inheritdoc} - */ - public function has(string $type) + public function has(string $type): bool { return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; } - /** - * {@inheritdoc} - */ - public function keys() + public function keys(): array { return array_keys($this->flashes['display']); } - /** - * {@inheritdoc} - */ - public function getStorageKey() + public function getStorageKey(): string { return $this->storageKey; } - /** - * {@inheritdoc} - */ - public function clear() + public function clear(): mixed { return $this->all(); } diff --git a/symfony/http-foundation/Session/Flash/FlashBag.php b/symfony/http-foundation/Session/Flash/FlashBag.php index 88df7508a..659d59d18 100644 --- a/symfony/http-foundation/Session/Flash/FlashBag.php +++ b/symfony/http-foundation/Session/Flash/FlashBag.php @@ -18,9 +18,9 @@ */ class FlashBag implements FlashBagInterface { - private $name = 'flashes'; - private $flashes = []; - private $storageKey; + private string $name = 'flashes'; + private array $flashes = []; + private string $storageKey; /** * @param string $storageKey The key used to store flashes in the session @@ -30,21 +30,21 @@ public function __construct(string $storageKey = '_symfony_flashes') $this->storageKey = $storageKey; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } /** - * {@inheritdoc} + * @return void */ public function initialize(array &$flashes) { @@ -52,33 +52,24 @@ public function initialize(array &$flashes) } /** - * {@inheritdoc} + * @return void */ - public function add(string $type, $message) + public function add(string $type, mixed $message) { $this->flashes[$type][] = $message; } - /** - * {@inheritdoc} - */ - public function peek(string $type, array $default = []) + public function peek(string $type, array $default = []): array { return $this->has($type) ? $this->flashes[$type] : $default; } - /** - * {@inheritdoc} - */ - public function peekAll() + public function peekAll(): array { return $this->flashes; } - /** - * {@inheritdoc} - */ - public function get(string $type, array $default = []) + public function get(string $type, array $default = []): array { if (!$this->has($type)) { return $default; @@ -91,10 +82,7 @@ public function get(string $type, array $default = []) return $return; } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { $return = $this->peekAll(); $this->flashes = []; @@ -103,49 +91,37 @@ public function all() } /** - * {@inheritdoc} + * @return void */ - public function set(string $type, $messages) + public function set(string $type, string|array $messages) { $this->flashes[$type] = (array) $messages; } /** - * {@inheritdoc} + * @return void */ public function setAll(array $messages) { $this->flashes = $messages; } - /** - * {@inheritdoc} - */ - public function has(string $type) + public function has(string $type): bool { return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; } - /** - * {@inheritdoc} - */ - public function keys() + public function keys(): array { return array_keys($this->flashes); } - /** - * {@inheritdoc} - */ - public function getStorageKey() + public function getStorageKey(): string { return $this->storageKey; } - /** - * {@inheritdoc} - */ - public function clear() + public function clear(): mixed { return $this->all(); } diff --git a/symfony/http-foundation/Session/Flash/FlashBagInterface.php b/symfony/http-foundation/Session/Flash/FlashBagInterface.php index 8713e71d0..bbcf7f8b7 100644 --- a/symfony/http-foundation/Session/Flash/FlashBagInterface.php +++ b/symfony/http-foundation/Session/Flash/FlashBagInterface.php @@ -23,66 +23,56 @@ interface FlashBagInterface extends SessionBagInterface /** * Adds a flash message for the given type. * - * @param mixed $message + * @return void */ - public function add(string $type, $message); + public function add(string $type, mixed $message); /** * Registers one or more messages for a given type. * - * @param string|array $messages + * @return void */ - public function set(string $type, $messages); + public function set(string $type, string|array $messages); /** * Gets flash messages for a given type. * * @param string $type Message category type * @param array $default Default value if $type does not exist - * - * @return array */ - public function peek(string $type, array $default = []); + public function peek(string $type, array $default = []): array; /** * Gets all flash messages. - * - * @return array */ - public function peekAll(); + public function peekAll(): array; /** * Gets and clears flash from the stack. * * @param array $default Default value if $type does not exist - * - * @return array */ - public function get(string $type, array $default = []); + public function get(string $type, array $default = []): array; /** * Gets and clears flashes from the stack. - * - * @return array */ - public function all(); + public function all(): array; /** * Sets all flash messages. + * + * @return void */ public function setAll(array $messages); /** * Has flash messages for a given type? - * - * @return bool */ - public function has(string $type); + public function has(string $type): bool; /** * Returns a list of all defined types. - * - * @return array */ - public function keys(); + public function keys(): array; } diff --git a/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php b/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php new file mode 100644 index 000000000..90151d38d --- /dev/null +++ b/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; + +/** + * Interface for session with a flashbag. + */ +interface FlashBagAwareSessionInterface extends SessionInterface +{ + public function getFlashBag(): FlashBagInterface; +} diff --git a/symfony/http-foundation/Session/Session.php b/symfony/http-foundation/Session/Session.php index 022e3986f..5b6db1754 100644 --- a/symfony/http-foundation/Session/Session.php +++ b/symfony/http-foundation/Session/Session.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; @@ -29,98 +30,80 @@ class_exists(SessionBagProxy::class); * * @implements \IteratorAggregate */ -class Session implements SessionInterface, \IteratorAggregate, \Countable +class Session implements FlashBagAwareSessionInterface, \IteratorAggregate, \Countable { protected $storage; - private $flashName; - private $attributeName; - private $data = []; - private $usageIndex = 0; - private $usageReporter; + private string $flashName; + private string $attributeName; + private array $data = []; + private int $usageIndex = 0; + private ?\Closure $usageReporter; - public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null) + public function __construct(?SessionStorageInterface $storage = null, ?AttributeBagInterface $attributes = null, ?FlashBagInterface $flashes = null, ?callable $usageReporter = null) { $this->storage = $storage ?? new NativeSessionStorage(); - $this->usageReporter = $usageReporter; + $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); - $attributes = $attributes ?? new AttributeBag(); + $attributes ??= new AttributeBag(); $this->attributeName = $attributes->getName(); $this->registerBag($attributes); - $flashes = $flashes ?? new FlashBag(); + $flashes ??= new FlashBag(); $this->flashName = $flashes->getName(); $this->registerBag($flashes); } - /** - * {@inheritdoc} - */ - public function start() + public function start(): bool { return $this->storage->start(); } - /** - * {@inheritdoc} - */ - public function has(string $name) + public function has(string $name): bool { return $this->getAttributeBag()->has($name); } - /** - * {@inheritdoc} - */ - public function get(string $name, $default = null) + public function get(string $name, mixed $default = null): mixed { return $this->getAttributeBag()->get($name, $default); } /** - * {@inheritdoc} + * @return void */ - public function set(string $name, $value) + public function set(string $name, mixed $value) { $this->getAttributeBag()->set($name, $value); } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { return $this->getAttributeBag()->all(); } /** - * {@inheritdoc} + * @return void */ public function replace(array $attributes) { $this->getAttributeBag()->replace($attributes); } - /** - * {@inheritdoc} - */ - public function remove(string $name) + public function remove(string $name): mixed { return $this->getAttributeBag()->remove($name); } /** - * {@inheritdoc} + * @return void */ public function clear() { $this->getAttributeBag()->clear(); } - /** - * {@inheritdoc} - */ - public function isStarted() + public function isStarted(): bool { return $this->storage->isStarted(); } @@ -130,19 +113,15 @@ public function isStarted() * * @return \ArrayIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->getAttributeBag()->all()); } /** * Returns the number of attributes. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->getAttributeBag()->all()); } @@ -172,42 +151,33 @@ public function isEmpty(): bool return true; } - /** - * {@inheritdoc} - */ - public function invalidate(int $lifetime = null) + public function invalidate(?int $lifetime = null): bool { $this->storage->clear(); return $this->migrate(true, $lifetime); } - /** - * {@inheritdoc} - */ - public function migrate(bool $destroy = false, int $lifetime = null) + public function migrate(bool $destroy = false, ?int $lifetime = null): bool { return $this->storage->regenerate($destroy, $lifetime); } /** - * {@inheritdoc} + * @return void */ public function save() { $this->storage->save(); } - /** - * {@inheritdoc} - */ - public function getId() + public function getId(): string { return $this->storage->getId(); } /** - * {@inheritdoc} + * @return void */ public function setId(string $id) { @@ -216,26 +186,20 @@ public function setId(string $id) } } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->storage->getName(); } /** - * {@inheritdoc} + * @return void */ public function setName(string $name) { $this->storage->setName($name); } - /** - * {@inheritdoc} - */ - public function getMetadataBag() + public function getMetadataBag(): MetadataBag { ++$this->usageIndex; if ($this->usageReporter && 0 <= $this->usageIndex) { @@ -246,17 +210,14 @@ public function getMetadataBag() } /** - * {@inheritdoc} + * @return void */ public function registerBag(SessionBagInterface $bag) { $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); } - /** - * {@inheritdoc} - */ - public function getBag(string $name) + public function getBag(string $name): SessionBagInterface { $bag = $this->storage->getBag($name); @@ -265,10 +226,8 @@ public function getBag(string $name) /** * Gets the flashbag interface. - * - * @return FlashBagInterface */ - public function getFlashBag() + public function getFlashBag(): FlashBagInterface { return $this->getBag($this->flashName); } diff --git a/symfony/http-foundation/Session/SessionBagInterface.php b/symfony/http-foundation/Session/SessionBagInterface.php index 8e37d06d6..e1c250554 100644 --- a/symfony/http-foundation/Session/SessionBagInterface.php +++ b/symfony/http-foundation/Session/SessionBagInterface.php @@ -20,27 +20,25 @@ interface SessionBagInterface { /** * Gets this bag's name. - * - * @return string */ - public function getName(); + public function getName(): string; /** * Initializes the Bag. + * + * @return void */ public function initialize(array &$array); /** * Gets the storage key for this bag. - * - * @return string */ - public function getStorageKey(); + public function getStorageKey(): string; /** * Clears out data from bag. * * @return mixed Whatever data was contained */ - public function clear(); + public function clear(): mixed; } diff --git a/symfony/http-foundation/Session/SessionBagProxy.php b/symfony/http-foundation/Session/SessionBagProxy.php index 90aa010c9..e759d94db 100644 --- a/symfony/http-foundation/Session/SessionBagProxy.php +++ b/symfony/http-foundation/Session/SessionBagProxy.php @@ -18,17 +18,17 @@ */ final class SessionBagProxy implements SessionBagInterface { - private $bag; - private $data; - private $usageIndex; - private $usageReporter; + private SessionBagInterface $bag; + private array $data; + private ?int $usageIndex; + private ?\Closure $usageReporter; public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter) { $this->bag = $bag; $this->data = &$data; $this->usageIndex = &$usageIndex; - $this->usageReporter = $usageReporter; + $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); } public function getBag(): SessionBagInterface @@ -54,17 +54,11 @@ public function isEmpty(): bool return empty($this->data[$this->bag->getStorageKey()]); } - /** - * {@inheritdoc} - */ public function getName(): string { return $this->bag->getName(); } - /** - * {@inheritdoc} - */ public function initialize(array &$array): void { ++$this->usageIndex; @@ -77,18 +71,12 @@ public function initialize(array &$array): void $this->bag->initialize($array); } - /** - * {@inheritdoc} - */ public function getStorageKey(): string { return $this->bag->getStorageKey(); } - /** - * {@inheritdoc} - */ - public function clear() + public function clear(): mixed { return $this->bag->clear(); } diff --git a/symfony/http-foundation/Session/SessionFactory.php b/symfony/http-foundation/Session/SessionFactory.php index 04c4b06a0..c06ed4b7d 100644 --- a/symfony/http-foundation/Session/SessionFactory.php +++ b/symfony/http-foundation/Session/SessionFactory.php @@ -22,15 +22,15 @@ class_exists(Session::class); */ class SessionFactory implements SessionFactoryInterface { - private $requestStack; - private $storageFactory; - private $usageReporter; + private RequestStack $requestStack; + private SessionStorageFactoryInterface $storageFactory; + private ?\Closure $usageReporter; - public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null) + public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, ?callable $usageReporter = null) { $this->requestStack = $requestStack; $this->storageFactory = $storageFactory; - $this->usageReporter = $usageReporter; + $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); } public function createSession(): SessionInterface diff --git a/symfony/http-foundation/Session/SessionInterface.php b/symfony/http-foundation/Session/SessionInterface.php index e67338337..07785a6f4 100644 --- a/symfony/http-foundation/Session/SessionInterface.php +++ b/symfony/http-foundation/Session/SessionInterface.php @@ -23,33 +23,31 @@ interface SessionInterface /** * Starts the session storage. * - * @return bool - * * @throws \RuntimeException if session fails to start */ - public function start(); + public function start(): bool; /** * Returns the session ID. - * - * @return string */ - public function getId(); + public function getId(): string; /** * Sets the session ID. + * + * @return void */ public function setId(string $id); /** * Returns the session name. - * - * @return string */ - public function getName(); + public function getName(): string; /** * Sets the session name. + * + * @return void */ public function setName(string $name); @@ -63,10 +61,8 @@ public function setName(string $name); * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. - * - * @return bool */ - public function invalidate(int $lifetime = null); + public function invalidate(?int $lifetime = null): bool; /** * Migrates the current session to a new session id while maintaining all @@ -77,10 +73,8 @@ public function invalidate(int $lifetime = null); * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. - * - * @return bool */ - public function migrate(bool $destroy = false, int $lifetime = null); + public function migrate(bool $destroy = false, ?int $lifetime = null): bool; /** * Force the session to be saved and closed. @@ -88,41 +82,37 @@ public function migrate(bool $destroy = false, int $lifetime = null); * This method is generally not required for real sessions as * the session will be automatically saved at the end of * code execution. + * + * @return void */ public function save(); /** * Checks if an attribute is defined. - * - * @return bool */ - public function has(string $name); + public function has(string $name): bool; /** * Returns an attribute. - * - * @param mixed $default The default value if not found - * - * @return mixed */ - public function get(string $name, $default = null); + public function get(string $name, mixed $default = null): mixed; /** * Sets an attribute. * - * @param mixed $value + * @return void */ - public function set(string $name, $value); + public function set(string $name, mixed $value); /** * Returns attributes. - * - * @return array */ - public function all(); + public function all(): array; /** * Sets attributes. + * + * @return void */ public function replace(array $attributes); @@ -131,36 +121,34 @@ public function replace(array $attributes); * * @return mixed The removed value or null when it does not exist */ - public function remove(string $name); + public function remove(string $name): mixed; /** * Clears all attributes. + * + * @return void */ public function clear(); /** * Checks if the session was started. - * - * @return bool */ - public function isStarted(); + public function isStarted(): bool; /** * Registers a SessionBagInterface with the session. + * + * @return void */ public function registerBag(SessionBagInterface $bag); /** * Gets a bag instance by name. - * - * @return SessionBagInterface */ - public function getBag(string $name); + public function getBag(string $name): SessionBagInterface; /** * Gets session meta. - * - * @return MetadataBag */ - public function getMetadataBag(); + public function getMetadataBag(): MetadataBag; } diff --git a/symfony/http-foundation/Session/SessionUtils.php b/symfony/http-foundation/Session/SessionUtils.php index b5bce4a88..504c5848e 100644 --- a/symfony/http-foundation/Session/SessionUtils.php +++ b/symfony/http-foundation/Session/SessionUtils.php @@ -25,7 +25,7 @@ final class SessionUtils * Finds the session header amongst the headers that are to be sent, removes it, and returns * it so the caller can process it further. */ - public static function popSessionCookie(string $sessionName, string $sessionId): ?string + public static function popSessionCookie(string $sessionName, #[\SensitiveParameter] string $sessionId): ?string { $sessionCookie = null; $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); diff --git a/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php index 35d7b4b81..288c24232 100644 --- a/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php @@ -22,17 +22,13 @@ */ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - private $sessionName; - private $prefetchId; - private $prefetchData; - private $newSessionId; - private $igbinaryEmptyData; - - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function open($savePath, $sessionName) + private string $sessionName; + private string $prefetchId; + private string $prefetchData; + private ?string $newSessionId = null; + private string $igbinaryEmptyData; + + public function open(string $savePath, string $sessionName): bool { $this->sessionName = $sessionName; if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { @@ -42,52 +38,26 @@ public function open($savePath, $sessionName) return true; } - /** - * @return string - */ - abstract protected function doRead(string $sessionId); - - /** - * @return bool - */ - abstract protected function doWrite(string $sessionId, string $data); - - /** - * @return bool - */ - abstract protected function doDestroy(string $sessionId); - - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function validateId($sessionId) + abstract protected function doRead(#[\SensitiveParameter] string $sessionId): string; + + abstract protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool; + + abstract protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool; + + public function validateId(#[\SensitiveParameter] string $sessionId): bool { $this->prefetchData = $this->read($sessionId); $this->prefetchId = $sessionId; - if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) { - // work around https://bugs.php.net/79413 - foreach (debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { - if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) { - return '' === $this->prefetchData; - } - } - } - return '' !== $this->prefetchData; } - /** - * @return string - */ - #[\ReturnTypeWillChange] - public function read($sessionId) + public function read(#[\SensitiveParameter] string $sessionId): string { - if (null !== $this->prefetchId) { + if (isset($this->prefetchId)) { $prefetchId = $this->prefetchId; $prefetchData = $this->prefetchData; - $this->prefetchId = $this->prefetchData = null; + unset($this->prefetchId, $this->prefetchData); if ($prefetchId === $sessionId || '' === $prefetchData) { $this->newSessionId = '' === $prefetchData ? $sessionId : null; @@ -102,16 +72,10 @@ public function read($sessionId) return $data; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function write($sessionId, $data) + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool { - if (null === $this->igbinaryEmptyData) { - // see https://github.com/igbinary/igbinary/issues/146 - $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; - } + // see https://github.com/igbinary/igbinary/issues/146 + $this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; if ('' === $data || $this->igbinaryEmptyData === $data) { return $this->destroy($sessionId); } @@ -120,14 +84,10 @@ public function write($sessionId, $data) return $this->doWrite($sessionId, $data); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function destroy($sessionId) + public function destroy(#[\SensitiveParameter] string $sessionId): bool { - if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) { - if (!$this->sessionName) { + if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) { + if (!isset($this->sessionName)) { throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); } $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); @@ -140,13 +100,9 @@ public function destroy($sessionId) * started the session). */ if (null === $cookie || isset($_COOKIE[$this->sessionName])) { - if (\PHP_VERSION_ID < 70300) { - setcookie($this->sessionName, '', 0, \ini_get('session.cookie_path'), \ini_get('session.cookie_domain'), filter_var(\ini_get('session.cookie_secure'), \FILTER_VALIDATE_BOOLEAN), filter_var(\ini_get('session.cookie_httponly'), \FILTER_VALIDATE_BOOLEAN)); - } else { - $params = session_get_cookie_params(); - unset($params['lifetime']); - setcookie($this->sessionName, '', $params); - } + $params = session_get_cookie_params(); + unset($params['lifetime']); + setcookie($this->sessionName, '', $params); } } diff --git a/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php index bea3a323e..411a8d1f0 100644 --- a/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php +++ b/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php @@ -18,9 +18,6 @@ */ class IdentityMarshaller implements MarshallerInterface { - /** - * {@inheritdoc} - */ public function marshall(array $values, ?array &$failed): array { foreach ($values as $key => $value) { @@ -32,9 +29,6 @@ public function marshall(array $values, ?array &$failed): array return $values; } - /** - * {@inheritdoc} - */ public function unmarshall(string $value): string { return $value; diff --git a/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php index c321c8c93..1567f5433 100644 --- a/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -18,8 +18,8 @@ */ class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - private $handler; - private $marshaller; + private AbstractSessionHandler $handler; + private MarshallerInterface $marshaller; public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) { @@ -27,56 +27,32 @@ public function __construct(AbstractSessionHandler $handler, MarshallerInterface $this->marshaller = $marshaller; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function open($savePath, $name) + public function open(string $savePath, string $name): bool { return $this->handler->open($savePath, $name); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { return $this->handler->close(); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function destroy($sessionId) + public function destroy(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->destroy($sessionId); } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { return $this->handler->gc($maxlifetime); } - /** - * @return string - */ - #[\ReturnTypeWillChange] - public function read($sessionId) + public function read(#[\SensitiveParameter] string $sessionId): string { return $this->marshaller->unmarshall($this->handler->read($sessionId)); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function write($sessionId, $data) + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool { $failed = []; $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); @@ -88,20 +64,12 @@ public function write($sessionId, $data) return $this->handler->write($sessionId, $marshalledData['data']); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function validateId($sessionId) + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->validateId($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler->updateTimestamp($sessionId, $data); } diff --git a/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php index e0ec4d2d9..91a023ddb 100644 --- a/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -21,17 +21,17 @@ */ class MemcachedSessionHandler extends AbstractSessionHandler { - private $memcached; + private \Memcached $memcached; /** - * @var int Time to live in seconds + * Time to live in seconds. */ - private $ttl; + private int|\Closure|null $ttl; /** - * @var string Key prefix for shared environments + * Key prefix for shared environments. */ - private $prefix; + private string $prefix; /** * Constructor. @@ -54,45 +54,31 @@ public function __construct(\Memcached $memcached, array $options = []) $this->prefix = $options['prefix'] ?? 'sf2s'; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { return $this->memcached->quit(); } - /** - * {@inheritdoc} - */ - protected function doRead(string $sessionId) + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return $this->memcached->get($this->prefix.$sessionId) ?: ''; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); return true; } - /** - * {@inheritdoc} - */ - protected function doWrite(string $sessionId, string $data) + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); } private function getCompatibleTtl(): int { - $ttl = (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')); + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time. // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct. @@ -103,21 +89,14 @@ private function getCompatibleTtl(): int return $ttl; } - /** - * {@inheritdoc} - */ - protected function doDestroy(string $sessionId) + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { $result = $this->memcached->delete($this->prefix.$sessionId); return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { // not required here because memcached will auto expire the records anyhow. return 0; @@ -125,10 +104,8 @@ public function gc($maxlifetime) /** * Return a Memcached instance. - * - * @return \Memcached */ - protected function getMemcached() + protected function getMemcached(): \Memcached { return $this->memcached; } diff --git a/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php index bf27ca6cc..8ed6a7b3f 100644 --- a/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php @@ -22,15 +22,8 @@ */ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - /** - * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface - */ - private $currentHandler; - - /** - * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface - */ - private $writeOnlyHandler; + private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $currentHandler; + private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $writeOnlyHandler; public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) { @@ -45,11 +38,7 @@ public function __construct(\SessionHandlerInterface $currentHandler, \SessionHa $this->writeOnlyHandler = $writeOnlyHandler; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { $result = $this->currentHandler->close(); $this->writeOnlyHandler->close(); @@ -57,11 +46,7 @@ public function close() return $result; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function destroy($sessionId) + public function destroy(#[\SensitiveParameter] string $sessionId): bool { $result = $this->currentHandler->destroy($sessionId); $this->writeOnlyHandler->destroy($sessionId); @@ -69,11 +54,7 @@ public function destroy($sessionId) return $result; } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { $result = $this->currentHandler->gc($maxlifetime); $this->writeOnlyHandler->gc($maxlifetime); @@ -81,11 +62,7 @@ public function gc($maxlifetime) return $result; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function open($savePath, $sessionName) + public function open(string $savePath, string $sessionName): bool { $result = $this->currentHandler->open($savePath, $sessionName); $this->writeOnlyHandler->open($savePath, $sessionName); @@ -93,21 +70,13 @@ public function open($savePath, $sessionName) return $result; } - /** - * @return string - */ - #[\ReturnTypeWillChange] - public function read($sessionId) + public function read(#[\SensitiveParameter] string $sessionId): string { // No reading from new handler until switch-over return $this->currentHandler->read($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function write($sessionId, $sessionData) + public function write(#[\SensitiveParameter] string $sessionId, string $sessionData): bool { $result = $this->currentHandler->write($sessionId, $sessionData); $this->writeOnlyHandler->write($sessionId, $sessionData); @@ -115,21 +84,13 @@ public function write($sessionId, $sessionData) return $result; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function validateId($sessionId) + public function validateId(#[\SensitiveParameter] string $sessionId): bool { // No reading from new handler until switch-over return $this->currentHandler->validateId($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $sessionData) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $sessionData): bool { $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); diff --git a/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php index ef8f71942..d5586030f 100644 --- a/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -14,29 +14,24 @@ use MongoDB\BSON\Binary; use MongoDB\BSON\UTCDateTime; use MongoDB\Client; -use MongoDB\Collection; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Manager; +use MongoDB\Driver\Query; /** - * Session handler using the mongodb/mongodb package and MongoDB driver extension. + * Session handler using the MongoDB driver extension. * * @author Markus Bachmann + * @author Jérôme Tamarelle * - * @see https://packagist.org/packages/mongodb/mongodb * @see https://php.net/mongodb */ class MongoDbSessionHandler extends AbstractSessionHandler { - private $mongo; - - /** - * @var Collection - */ - private $collection; - - /** - * @var array - */ - private $options; + private Manager $manager; + private string $namespace; + private array $options; + private int|\Closure|null $ttl; /** * Constructor. @@ -47,7 +42,8 @@ class MongoDbSessionHandler extends AbstractSessionHandler * * id_field: The field name for storing the session id [default: _id] * * data_field: The field name for storing the session data [default: data] * * time_field: The field name for storing the timestamp [default: time] - * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. + * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at] + * * ttl: The time to live in seconds. * * It is strongly recommended to put an index on the `expiry_field` for * garbage-collection. Alternatively it's possible to automatically expire @@ -68,13 +64,18 @@ class MongoDbSessionHandler extends AbstractSessionHandler * * @throws \InvalidArgumentException When "database" or "collection" not provided */ - public function __construct(Client $mongo, array $options) + public function __construct(Client|Manager $mongo, array $options) { if (!isset($options['database']) || !isset($options['collection'])) { throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.'); } - $this->mongo = $mongo; + if ($mongo instanceof Client) { + $mongo = $mongo->getManager(); + } + + $this->manager = $mongo; + $this->namespace = $options['database'].'.'.$options['collection']; $this->options = array_merge([ 'id_field' => '_id', @@ -82,112 +83,104 @@ public function __construct(Client $mongo, array $options) 'time_field' => 'time', 'expiry_field' => 'expires_at', ], $options); + $this->ttl = $this->options['ttl'] ?? null; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { return true; } - /** - * {@inheritdoc} - */ - protected function doDestroy(string $sessionId) + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { - $this->getCollection()->deleteOne([ - $this->options['id_field'] => $sessionId, - ]); + $write = new BulkWrite(); + $write->delete( + [$this->options['id_field'] => $sessionId], + ['limit' => 1] + ); + + $this->manager->executeBulkWrite($this->namespace, $write); return true; } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { - return $this->getCollection()->deleteMany([ - $this->options['expiry_field'] => ['$lt' => new UTCDateTime()], - ])->getDeletedCount(); + $write = new BulkWrite(); + $write->delete( + [$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]], + ); + $result = $this->manager->executeBulkWrite($this->namespace, $write); + + return $result->getDeletedCount() ?? false; } - /** - * {@inheritdoc} - */ - protected function doWrite(string $sessionId, string $data) + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { - $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + $expiry = $this->getUTCDateTime($ttl); $fields = [ - $this->options['time_field'] => new UTCDateTime(), + $this->options['time_field'] => $this->getUTCDateTime(), $this->options['expiry_field'] => $expiry, - $this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY), + $this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC), ]; - $this->getCollection()->updateOne( + $write = new BulkWrite(); + $write->update( [$this->options['id_field'] => $sessionId], ['$set' => $fields], ['upsert' => true] ); + $this->manager->executeBulkWrite($this->namespace, $write); + return true; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { - $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + $expiry = $this->getUTCDateTime($ttl); - $this->getCollection()->updateOne( + $write = new BulkWrite(); + $write->update( [$this->options['id_field'] => $sessionId], ['$set' => [ - $this->options['time_field'] => new UTCDateTime(), + $this->options['time_field'] => $this->getUTCDateTime(), $this->options['expiry_field'] => $expiry, - ]] + ]], + ['multi' => false], ); + $this->manager->executeBulkWrite($this->namespace, $write); + return true; } - /** - * {@inheritdoc} - */ - protected function doRead(string $sessionId) + protected function doRead(#[\SensitiveParameter] string $sessionId): string { - $dbData = $this->getCollection()->findOne([ + $cursor = $this->manager->executeQuery($this->namespace, new Query([ $this->options['id_field'] => $sessionId, - $this->options['expiry_field'] => ['$gte' => new UTCDateTime()], - ]); - - if (null === $dbData) { - return ''; + $this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()], + ], [ + 'projection' => [ + '_id' => false, + $this->options['data_field'] => true, + ], + 'limit' => 1, + ])); + + foreach ($cursor as $document) { + return (string) $document->{$this->options['data_field']} ?? ''; } - return $dbData[$this->options['data_field']]->getData(); + // Not found + return ''; } - private function getCollection(): Collection - { - if (null === $this->collection) { - $this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']); - } - - return $this->collection; - } - - /** - * @return Client - */ - protected function getMongo() + private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime { - return $this->mongo; + return new UTCDateTime((time() + $additionalSeconds) * 1000); } } diff --git a/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php index 52a103879..f8c6151a4 100644 --- a/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php @@ -28,13 +28,9 @@ class NativeFileSessionHandler extends \SessionHandler * @throws \InvalidArgumentException On invalid $savePath * @throws \RuntimeException When failing to create the save directory */ - public function __construct(string $savePath = null) + public function __construct(?string $savePath = null) { - if (null === $savePath) { - $savePath = \ini_get('session.save_path'); - } - - $baseDir = $savePath; + $baseDir = $savePath ??= \ini_get('session.save_path'); if ($count = substr_count($savePath, ';')) { if ($count > 2) { diff --git a/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php index 4331dbe50..a77185e2e 100644 --- a/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php @@ -18,62 +18,37 @@ */ class NullSessionHandler extends AbstractSessionHandler { - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { return true; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function validateId($sessionId) + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return true; } - /** - * {@inheritdoc} - */ - protected function doRead(string $sessionId) + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return ''; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return true; } - /** - * {@inheritdoc} - */ - protected function doWrite(string $sessionId, string $data) + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { return true; } - /** - * {@inheritdoc} - */ - protected function doDestroy(string $sessionId) + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { return true; } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { return 0; } diff --git a/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php index cad7e0a72..9cee76ddf 100644 --- a/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php @@ -11,6 +11,9 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\Types; + /** * Session handler using a PDO connection to read and write data. * @@ -65,105 +68,66 @@ class PdoSessionHandler extends AbstractSessionHandler */ public const LOCK_TRANSACTIONAL = 2; - private const MAX_LIFETIME = 315576000; - - /** - * @var \PDO|null PDO instance or null when not connected yet - */ - private $pdo; + private \PDO $pdo; /** * DSN string or null for session.save_path or false when lazy connection disabled. - * - * @var string|false|null */ - private $dsn = false; + private string|false|null $dsn = false; - /** - * @var string|null - */ - private $driver; + private string $driver; + private string $table = 'sessions'; + private string $idCol = 'sess_id'; + private string $dataCol = 'sess_data'; + private string $lifetimeCol = 'sess_lifetime'; + private string $timeCol = 'sess_time'; /** - * @var string + * Time to live in seconds. */ - private $table = 'sessions'; - - /** - * @var string - */ - private $idCol = 'sess_id'; - - /** - * @var string - */ - private $dataCol = 'sess_data'; - - /** - * @var string - */ - private $lifetimeCol = 'sess_lifetime'; - - /** - * @var string - */ - private $timeCol = 'sess_time'; + private int|\Closure|null $ttl; /** * Username when lazy-connect. - * - * @var string */ - private $username = ''; + private ?string $username = null; /** * Password when lazy-connect. - * - * @var string */ - private $password = ''; + private ?string $password = null; /** * Connection options when lazy-connect. - * - * @var array */ - private $connectionOptions = []; + private array $connectionOptions = []; /** * The strategy for locking, see constants. - * - * @var int */ - private $lockMode = self::LOCK_TRANSACTIONAL; + private int $lockMode = self::LOCK_TRANSACTIONAL; /** * It's an array to support multiple reads before closing which is manual, non-standard usage. * * @var \PDOStatement[] An array of statements to release advisory locks */ - private $unlockStatements = []; + private array $unlockStatements = []; /** * True when the current session exists but expired according to session.gc_maxlifetime. - * - * @var bool */ - private $sessionExpired = false; + private bool $sessionExpired = false; /** * Whether a transaction is active. - * - * @var bool */ - private $inTransaction = false; + private bool $inTransaction = false; /** * Whether gc() has been called. - * - * @var bool */ - private $gcCalled = false; + private bool $gcCalled = false; /** * You can either pass an existing database connection as PDO instance or @@ -181,12 +145,13 @@ class PdoSessionHandler extends AbstractSessionHandler * * db_password: The password when lazy-connect [default: ''] * * db_connection_options: An array of driver-specific connection options [default: []] * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] + * * ttl: The time to live in seconds. * * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null * * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION */ - public function __construct($pdoOrDsn = null, array $options = []) + public function __construct(#[\SensitiveParameter] \PDO|string|null $pdoOrDsn = null, #[\SensitiveParameter] array $options = []) { if ($pdoOrDsn instanceof \PDO) { if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { @@ -210,52 +175,94 @@ public function __construct($pdoOrDsn = null, array $options = []) $this->password = $options['db_password'] ?? $this->password; $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions; $this->lockMode = $options['lock_mode'] ?? $this->lockMode; + $this->ttl = $options['ttl'] ?? null; } /** - * Creates the table to store sessions which can be called once for setup. - * - * Session ID is saved in a column of maximum length 128 because that is enough even - * for a 512 bit configured session.hash_function like Whirlpool. Session data is - * saved in a BLOB. One could also use a shorter inlined varbinary column - * if one was sure the data fits into it. - * - * @throws \PDOException When the table already exists - * @throws \DomainException When an unsupported PDO driver is used + * Adds the Table to the Schema if it doesn't exist. */ - public function createTable() + public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null): void { - // connect if we are not yet - $this->getConnection(); + if ($schema->hasTable($this->table) || ($isSameDatabase && !$isSameDatabase($this->getConnection()->exec(...)))) { + return; + } + $table = $schema->createTable($this->table); switch ($this->driver) { case 'mysql': - // We use varbinary for the ID column because it prevents unwanted conversions: - // - character set conversions between server and client - // - trailing space removal - // - case-insensitivity - // - language processing like é == e - $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB"; + $table->addColumn($this->idCol, Types::BINARY)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addOption('collate', 'utf8mb4_bin'); + $table->addOption('engine', 'InnoDB'); break; case 'sqlite': - $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $table->addColumn($this->idCol, Types::TEXT)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); break; case 'pgsql': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BINARY)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); break; case 'oci': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); break; case 'sqlsrv': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $table->addColumn($this->idCol, Types::TEXT)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); break; default: throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); } + $table->setPrimaryKey([$this->idCol]); + $table->addIndex([$this->lifetimeCol], $this->lifetimeCol.'_idx'); + } + + /** + * Creates the table to store sessions which can be called once for setup. + * + * Session ID is saved in a column of maximum length 128 because that is enough even + * for a 512 bit configured session.hash_function like Whirlpool. Session data is + * saved in a BLOB. One could also use a shorter inlined varbinary column + * if one was sure the data fits into it. + * + * @return void + * + * @throws \PDOException When the table already exists + * @throws \DomainException When an unsupported PDO driver is used + */ + public function createTable() + { + // connect if we are not yet + $this->getConnection(); + + $sql = match ($this->driver) { + // We use varbinary for the ID column because it prevents unwanted conversions: + // - character set conversions between server and client + // - trailing space removal + // - case-insensitivity + // - language processing like é == e + 'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB", + 'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + 'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + 'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + 'sqlsrv' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + default => throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)), + }; try { $this->pdo->exec($sql); - $this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)"); + $this->pdo->exec("CREATE INDEX {$this->lifetimeCol}_idx ON $this->table ($this->lifetimeCol)"); } catch (\PDOException $e) { $this->rollback(); @@ -267,34 +274,24 @@ public function createTable() * Returns true when the current session exists but expired according to session.gc_maxlifetime. * * Can be used to distinguish between a new session and one that expired due to inactivity. - * - * @return bool */ - public function isSessionExpired() + public function isSessionExpired(): bool { return $this->sessionExpired; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function open($savePath, $sessionName) + public function open(string $savePath, string $sessionName): bool { $this->sessionExpired = false; - if (null === $this->pdo) { + if (!isset($this->pdo)) { $this->connect($this->dsn ?: $savePath); } return parent::open($savePath, $sessionName); } - /** - * @return string - */ - #[\ReturnTypeWillChange] - public function read($sessionId) + public function read(#[\SensitiveParameter] string $sessionId): string { try { return parent::read($sessionId); @@ -305,11 +302,7 @@ public function read($sessionId) } } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. // This way, pruning expired sessions does not block them from being started while the current session is used. @@ -318,10 +311,7 @@ public function gc($maxlifetime) return 0; } - /** - * {@inheritdoc} - */ - protected function doDestroy(string $sessionId) + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { // delete the record associated with this id $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; @@ -339,12 +329,9 @@ protected function doDestroy(string $sessionId) return true; } - /** - * {@inheritdoc} - */ - protected function doWrite(string $sessionId, string $data) + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { - $maxlifetime = (int) \ini_get('session.gc_maxlifetime'); + $maxlifetime = (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime')); try { // We use a single MERGE SQL query when supported by the database. @@ -385,13 +372,9 @@ protected function doWrite(string $sessionId, string $data) return true; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { - $expiry = time() + (int) \ini_get('session.gc_maxlifetime'); + $expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime')); try { $updateStmt = $this->pdo->prepare( @@ -410,11 +393,7 @@ public function updateTimestamp($sessionId, $data) return true; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { $this->commit(); @@ -426,27 +405,14 @@ public function close() $this->gcCalled = false; // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time AND $this->lifetimeCol > :min"; + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); - $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT); - $stmt->execute(); - // to be removed in 6.0 - if ('mysql' === $this->driver) { - $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol + $this->timeCol < :time"; - } else { - $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol < :time - $this->timeCol"; - } - - $stmt = $this->pdo->prepare($legacySql); - $stmt->bindValue(':time', time(), \PDO::PARAM_INT); - $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT); $stmt->execute(); } if (false !== $this->dsn) { - $this->pdo = null; // only close lazy-connection - $this->driver = null; + unset($this->pdo, $this->driver); // only close lazy-connection } return true; @@ -455,7 +421,7 @@ public function close() /** * Lazy-connects to the database. */ - private function connect(string $dsn): void + private function connect(#[\SensitiveParameter] string $dsn): void { $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); @@ -467,7 +433,7 @@ private function connect(string $dsn): void * * @todo implement missing support for oci DSN (which look totally different from other PDO ones) */ - private function buildDsnFromUrl(string $dsnOrUrl): string + private function buildDsnFromUrl(#[\SensitiveParameter] string $dsnOrUrl): string { // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); @@ -533,7 +499,7 @@ private function buildDsnFromUrl(string $dsnOrUrl): string // If "unix_socket" is not in the query, we continue with the same process as pgsql // no break case 'pgsql': - $dsn ?? $dsn = 'pgsql:'; + $dsn ??= 'pgsql:'; if (isset($params['host']) && '' !== $params['host']) { $dsn .= 'host='.$params['host'].';'; @@ -649,10 +615,8 @@ private function rollback(): void * * We need to make sure we do not return session data that is already considered garbage according * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. - * - * @return string */ - protected function doRead(string $sessionId) + protected function doRead(#[\SensitiveParameter] string $sessionId): string { if (self::LOCK_ADVISORY === $this->lockMode) { $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); @@ -669,9 +633,6 @@ protected function doRead(string $sessionId) if ($sessionRows) { $expiry = (int) $sessionRows[0][1]; - if ($expiry <= self::MAX_LIFETIME) { - $expiry += $sessionRows[0][2]; - } if ($expiry < time()) { $this->sessionExpired = true; @@ -687,7 +648,7 @@ protected function doRead(string $sessionId) throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); } - if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOL) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { // In strict mode, session fixation is not possible: new sessions always start with a unique // random id, so that concurrency is not possible and this code path can be skipped. // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block @@ -726,7 +687,7 @@ protected function doRead(string $sessionId) * - for oci using DBMS_LOCK.REQUEST * - for sqlsrv using sp_getapplock with LockOwner = Session */ - private function doAdvisoryLock(string $sessionId): \PDOStatement + private function doAdvisoryLock(#[\SensitiveParameter] string $sessionId): \PDOStatement { switch ($this->driver) { case 'mysql': @@ -804,14 +765,13 @@ private function getSelectSql(): string if (self::LOCK_TRANSACTIONAL === $this->lockMode) { $this->beginTransaction(); - // selecting the time column should be removed in 6.0 switch ($this->driver) { case 'mysql': case 'oci': case 'pgsql': - return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; + return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; case 'sqlsrv': - return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; + return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; case 'sqlite': // we already locked when starting transaction break; @@ -820,13 +780,13 @@ private function getSelectSql(): string } } - return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id"; + return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id"; } /** * Returns an insert statement supported by the database for writing session data. */ - private function getInsertStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + private function getInsertStatement(#[\SensitiveParameter] string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement { switch ($this->driver) { case 'oci': @@ -853,7 +813,7 @@ private function getInsertStatement(string $sessionId, string $sessionData, int /** * Returns an update statement supported by the database for writing session data. */ - private function getUpdateStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + private function getUpdateStatement(#[\SensitiveParameter] string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement { switch ($this->driver) { case 'oci': @@ -880,7 +840,7 @@ private function getUpdateStatement(string $sessionId, string $sessionData, int /** * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. */ - private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement + private function getMergeStatement(#[\SensitiveParameter] string $sessionId, string $data, int $maxlifetime): ?\PDOStatement { switch (true) { case 'mysql' === $this->driver: @@ -929,12 +889,10 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife /** * Return a PDO instance. - * - * @return \PDO */ - protected function getConnection() + protected function getConnection(): \PDO { - if (null === $this->pdo) { + if (!isset($this->pdo)) { $this->connect($this->dsn ?: \ini_get('session.save_path')); } diff --git a/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php index 31954e677..b696eee4b 100644 --- a/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php @@ -12,8 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Predis\Response\ErrorInterface; -use Symfony\Component\Cache\Traits\RedisClusterProxy; -use Symfony\Component\Cache\Traits\RedisProxy; +use Relay\Relay; /** * Redis based session storage handler based on the Redis class @@ -23,78 +22,56 @@ */ class RedisSessionHandler extends AbstractSessionHandler { - private $redis; - /** - * @var string Key prefix for shared environments + * Key prefix for shared environments. */ - private $prefix; + private string $prefix; /** - * @var int Time to live in seconds + * Time to live in seconds. */ - private $ttl; + private int|\Closure|null $ttl; /** * List of available options: * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server * * ttl: The time to live in seconds. * - * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis - * * @throws \InvalidArgumentException When unsupported client or options are passed */ - public function __construct($redis, array $options = []) - { - if ( - !$redis instanceof \Redis && - !$redis instanceof \RedisArray && - !$redis instanceof \RedisCluster && - !$redis instanceof \Predis\ClientInterface && - !$redis instanceof RedisProxy && - !$redis instanceof RedisClusterProxy - ) { - throw new \InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis))); - } - + public function __construct( + private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, + array $options = [], + ) { if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); } - $this->redis = $redis; $this->prefix = $options['prefix'] ?? 'sf_s'; $this->ttl = $options['ttl'] ?? null; } - /** - * {@inheritdoc} - */ - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return $this->redis->get($this->prefix.$sessionId) ?: ''; } - /** - * {@inheritdoc} - */ - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { - $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')), $data); + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + $result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data); return $result && !$result instanceof ErrorInterface; } - /** - * {@inheritdoc} - */ - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { static $unlink = true; if ($unlink) { try { $unlink = false !== $this->redis->unlink($this->prefix.$sessionId); - } catch (\Throwable $e) { + } catch (\Throwable) { $unlink = false; } } @@ -106,32 +83,21 @@ protected function doDestroy(string $sessionId): bool return true; } - /** - * {@inheritdoc} - */ #[\ReturnTypeWillChange] public function close(): bool { return true; } - /** - * {@inheritdoc} - * - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { return 0; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { - return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime'))); + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + + return $this->redis->expire($this->prefix.$sessionId, (int) $ttl); } } diff --git a/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php index 39dc30c6f..ff5b70d81 100644 --- a/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -15,35 +15,31 @@ use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Tools\DsnParser; +use Relay\Relay; use Symfony\Component\Cache\Adapter\AbstractAdapter; -use Symfony\Component\Cache\Traits\RedisClusterProxy; -use Symfony\Component\Cache\Traits\RedisProxy; /** * @author Nicolas Grekas */ class SessionHandlerFactory { - /** - * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN - */ - public static function createHandler($connection): AbstractSessionHandler + public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler { - if (!\is_string($connection) && !\is_object($connection)) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection))); - } + if ($query = \is_string($connection) ? parse_url($connection) : false) { + parse_str($query['query'] ?? '', $query); - if ($options = \is_string($connection) ? parse_url($connection) : false) { - parse_str($options['query'] ?? '', $options); + if (($options['ttl'] ?? null) instanceof \Closure) { + $query['ttl'] = $options['ttl']; + } } + $options = ($query ?: []) + $options; switch (true) { case $connection instanceof \Redis: + case $connection instanceof Relay: case $connection instanceof \RedisArray: case $connection instanceof \RedisCluster: case $connection instanceof \Predis\ClientInterface: - case $connection instanceof RedisProxy: - case $connection instanceof RedisClusterProxy: return new RedisSessionHandler($connection); case $connection instanceof \Memcached: @@ -63,16 +59,16 @@ public static function createHandler($connection): AbstractSessionHandler case str_starts_with($connection, 'rediss:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { - throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection)); + throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); } $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); - return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1])); + return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1])); case str_starts_with($connection, 'pdo_oci://'): if (!class_exists(DriverManager::class)) { - throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); + throw new \InvalidArgumentException('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".'); } $connection[3] = '-'; $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; @@ -82,6 +78,7 @@ public static function createHandler($connection): AbstractSessionHandler } $connection = DriverManager::getConnection($params, $config); + // The condition should be removed once support for DBAL <3.3 is dropped $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); // no break; @@ -94,7 +91,7 @@ public static function createHandler($connection): AbstractSessionHandler case str_starts_with($connection, 'sqlsrv://'): case str_starts_with($connection, 'sqlite://'): case str_starts_with($connection, 'sqlite3://'): - return new PdoSessionHandler($connection, $options ?: []); + return new PdoSessionHandler($connection, $options); } throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection)); diff --git a/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php index f7c385f64..1f8668744 100644 --- a/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php @@ -18,8 +18,8 @@ */ class StrictSessionHandler extends AbstractSessionHandler { - private $handler; - private $doDestroy; + private \SessionHandlerInterface $handler; + private bool $doDestroy; public function __construct(\SessionHandlerInterface $handler) { @@ -40,47 +40,29 @@ public function isWrapper(): bool return $this->handler instanceof \SessionHandler; } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function open($savePath, $sessionName) + public function open(string $savePath, string $sessionName): bool { parent::open($savePath, $sessionName); return $this->handler->open($savePath, $sessionName); } - /** - * {@inheritdoc} - */ - protected function doRead(string $sessionId) + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return $this->handler->read($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->write($sessionId, $data); } - /** - * {@inheritdoc} - */ - protected function doWrite(string $sessionId, string $data) + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler->write($sessionId, $data); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function destroy($sessionId) + public function destroy(#[\SensitiveParameter] string $sessionId): bool { $this->doDestroy = true; $destroyed = parent::destroy($sessionId); @@ -88,30 +70,19 @@ public function destroy($sessionId) return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; } - /** - * {@inheritdoc} - */ - protected function doDestroy(string $sessionId) + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { $this->doDestroy = false; return $this->handler->destroy($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { return $this->handler->close(); } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { return $this->handler->gc($maxlifetime); } diff --git a/symfony/http-foundation/Session/Storage/MetadataBag.php b/symfony/http-foundation/Session/Storage/MetadataBag.php index 52d332094..5bb4cfbc7 100644 --- a/symfony/http-foundation/Session/Storage/MetadataBag.php +++ b/symfony/http-foundation/Session/Storage/MetadataBag.php @@ -26,15 +26,8 @@ class MetadataBag implements SessionBagInterface public const UPDATED = 'u'; public const LIFETIME = 'l'; - /** - * @var string - */ - private $name = '__metadata'; - - /** - * @var string - */ - private $storageKey; + private string $name = '__metadata'; + private string $storageKey; /** * @var array @@ -43,15 +36,10 @@ class MetadataBag implements SessionBagInterface /** * Unix timestamp. - * - * @var int */ - private $lastUsed; + private int $lastUsed; - /** - * @var int - */ - private $updateThreshold; + private int $updateThreshold; /** * @param string $storageKey The key used to store bag in the session @@ -64,7 +52,7 @@ public function __construct(string $storageKey = '_sf2_meta', int $updateThresho } /** - * {@inheritdoc} + * @return void */ public function initialize(array &$array) { @@ -84,10 +72,8 @@ public function initialize(array &$array) /** * Gets the lifetime that the session cookie was set with. - * - * @return int */ - public function getLifetime() + public function getLifetime(): int { return $this->meta[self::LIFETIME]; } @@ -99,16 +85,15 @@ public function getLifetime() * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. + * + * @return void */ - public function stampNew(int $lifetime = null) + public function stampNew(?int $lifetime = null) { $this->stampCreated($lifetime); } - /** - * {@inheritdoc} - */ - public function getStorageKey() + public function getStorageKey(): string { return $this->storageKey; } @@ -118,7 +103,7 @@ public function getStorageKey() * * @return int Unix timestamp */ - public function getCreated() + public function getCreated(): int { return $this->meta[self::CREATED]; } @@ -128,37 +113,33 @@ public function getCreated() * * @return int Unix timestamp */ - public function getLastUsed() + public function getLastUsed(): int { return $this->lastUsed; } - /** - * {@inheritdoc} - */ - public function clear() + public function clear(): mixed { // nothing to do return null; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } /** * Sets name. + * + * @return void */ public function setName(string $name) { $this->name = $name; } - private function stampCreated(int $lifetime = null): void + private function stampCreated(?int $lifetime = null): void { $timeStamp = time(); $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; diff --git a/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php b/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php index c5c2bb073..f02793d3e 100644 --- a/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php +++ b/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php @@ -62,21 +62,21 @@ class MockArraySessionStorage implements SessionStorageInterface */ protected $bags = []; - public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + public function __construct(string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) { $this->name = $name; $this->setMetadataBag($metaBag); } + /** + * @return void + */ public function setSessionData(array $array) { $this->data = $array; } - /** - * {@inheritdoc} - */ - public function start() + public function start(): bool { if ($this->started) { return true; @@ -91,10 +91,7 @@ public function start() return true; } - /** - * {@inheritdoc} - */ - public function regenerate(bool $destroy = false, int $lifetime = null) + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool { if (!$this->started) { $this->start(); @@ -106,16 +103,13 @@ public function regenerate(bool $destroy = false, int $lifetime = null) return true; } - /** - * {@inheritdoc} - */ - public function getId() + public function getId(): string { return $this->id; } /** - * {@inheritdoc} + * @return void */ public function setId(string $id) { @@ -126,16 +120,13 @@ public function setId(string $id) $this->id = $id; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } /** - * {@inheritdoc} + * @return void */ public function setName(string $name) { @@ -143,7 +134,7 @@ public function setName(string $name) } /** - * {@inheritdoc} + * @return void */ public function save() { @@ -156,7 +147,7 @@ public function save() } /** - * {@inheritdoc} + * @return void */ public function clear() { @@ -173,17 +164,14 @@ public function clear() } /** - * {@inheritdoc} + * @return void */ public function registerBag(SessionBagInterface $bag) { $this->bags[$bag->getName()] = $bag; } - /** - * {@inheritdoc} - */ - public function getBag(string $name) + public function getBag(string $name): SessionBagInterface { if (!isset($this->bags[$name])) { throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); @@ -196,29 +184,26 @@ public function getBag(string $name) return $this->bags[$name]; } - /** - * {@inheritdoc} - */ - public function isStarted() + public function isStarted(): bool { return $this->started; } - public function setMetadataBag(MetadataBag $bag = null) + /** + * @return void + */ + public function setMetadataBag(?MetadataBag $bag = null) { - if (null === $bag) { - $bag = new MetadataBag(); + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); } - - $this->metadataBag = $bag; + $this->metadataBag = $bag ?? new MetadataBag(); } /** * Gets the MetadataBag. - * - * @return MetadataBag */ - public function getMetadataBag() + public function getMetadataBag(): MetadataBag { return $this->metadataBag; } @@ -228,21 +213,22 @@ public function getMetadataBag() * * This doesn't need to be particularly cryptographically secure since this is just * a mock. - * - * @return string */ - protected function generateId() + protected function generateId(): string { - return hash('sha256', uniqid('ss_mock_', true)); + return bin2hex(random_bytes(16)); } + /** + * @return void + */ protected function loadSession() { $bags = array_merge($this->bags, [$this->metadataBag]); foreach ($bags as $bag) { $key = $bag->getStorageKey(); - $this->data[$key] = $this->data[$key] ?? []; + $this->data[$key] ??= []; $bag->initialize($this->data[$key]); } diff --git a/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php b/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php index 8e32a45e3..ef6d9d8f8 100644 --- a/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php +++ b/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php @@ -25,16 +25,14 @@ */ class MockFileSessionStorage extends MockArraySessionStorage { - private $savePath; + private string $savePath; /** * @param string|null $savePath Path of directory to save session files */ - public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) { - if (null === $savePath) { - $savePath = sys_get_temp_dir(); - } + $savePath ??= sys_get_temp_dir(); if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $savePath)); @@ -45,10 +43,7 @@ public function __construct(string $savePath = null, string $name = 'MOCKSESSID' parent::__construct($name, $metaBag); } - /** - * {@inheritdoc} - */ - public function start() + public function start(): bool { if ($this->started) { return true; @@ -65,10 +60,7 @@ public function start() return true; } - /** - * {@inheritdoc} - */ - public function regenerate(bool $destroy = false, int $lifetime = null) + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool { if (!$this->started) { $this->start(); @@ -82,7 +74,7 @@ public function regenerate(bool $destroy = false, int $lifetime = null) } /** - * {@inheritdoc} + * @return void */ public function save() { diff --git a/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php b/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php index d0da1e169..6727cf14f 100644 --- a/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php +++ b/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php @@ -21,14 +21,14 @@ class_exists(MockFileSessionStorage::class); */ class MockFileSessionStorageFactory implements SessionStorageFactoryInterface { - private $savePath; - private $name; - private $metaBag; + private ?string $savePath; + private string $name; + private ?MetadataBag $metaBag; /** * @see MockFileSessionStorage constructor. */ - public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) { $this->savePath = $savePath; $this->name = $name; diff --git a/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/symfony/http-foundation/Session/Storage/NativeSessionStorage.php index 242478c42..f63de5740 100644 --- a/symfony/http-foundation/Session/Storage/NativeSessionStorage.php +++ b/symfony/http-foundation/Session/Storage/NativeSessionStorage.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; -use Symfony\Component\HttpFoundation\Session\SessionUtils; use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; @@ -54,11 +53,6 @@ class NativeSessionStorage implements SessionStorageInterface */ protected $metadataBag; - /** - * @var string|null - */ - private $emulateSameSite; - /** * Depending on how you want the storage driver to behave you probably * want to override this constructor entirely. @@ -94,10 +88,8 @@ class NativeSessionStorage implements SessionStorageInterface * sid_bits_per_character, "5" * trans_sid_hosts, $_SERVER['HTTP_HOST'] * trans_sid_tags, "a=href,area=href,frame=src,form=" - * - * @param AbstractProxy|\SessionHandlerInterface|null $handler */ - public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null) + public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null) { if (!\extension_loaded('session')) { throw new \LogicException('PHP extension "session" is required.'); @@ -120,18 +112,13 @@ public function __construct(array $options = [], $handler = null, MetadataBag $m /** * Gets the save handler instance. - * - * @return AbstractProxy|\SessionHandlerInterface */ - public function getSaveHandler() + public function getSaveHandler(): AbstractProxy|\SessionHandlerInterface { return $this->saveHandler; } - /** - * {@inheritdoc} - */ - public function start() + public function start(): bool { if ($this->started) { return true; @@ -141,7 +128,7 @@ public function start() throw new \RuntimeException('Failed to start the session: already started by PHP.'); } - if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) { + if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL) && headers_sent($file, $line)) { throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); } @@ -186,54 +173,38 @@ public function start() throw new \RuntimeException('Failed to start the session.'); } - if (null !== $this->emulateSameSite) { - $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); - if (null !== $originalCookie) { - header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); - } - } - $this->loadSession(); return true; } - /** - * {@inheritdoc} - */ - public function getId() + public function getId(): string { return $this->saveHandler->getId(); } /** - * {@inheritdoc} + * @return void */ public function setId(string $id) { $this->saveHandler->setId($id); } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->saveHandler->getName(); } /** - * {@inheritdoc} + * @return void */ public function setName(string $name) { $this->saveHandler->setName($name); } - /** - * {@inheritdoc} - */ - public function regenerate(bool $destroy = false, int $lifetime = null) + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool { // Cannot regenerate the session ID for non-active sessions. if (\PHP_SESSION_ACTIVE !== session_status()) { @@ -254,20 +225,11 @@ public function regenerate(bool $destroy = false, int $lifetime = null) $this->metadataBag->stampNew(); } - $isRegenerated = session_regenerate_id($destroy); - - if (null !== $this->emulateSameSite) { - $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); - if (null !== $originalCookie) { - header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); - } - } - - return $isRegenerated; + return session_regenerate_id($destroy); } /** - * {@inheritdoc} + * @return void */ public function save() { @@ -287,7 +249,7 @@ public function save() $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) { $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; - $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler)); + $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', $handler::class); } return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; @@ -309,7 +271,7 @@ public function save() } /** - * {@inheritdoc} + * @return void */ public function clear() { @@ -326,7 +288,7 @@ public function clear() } /** - * {@inheritdoc} + * @return void */ public function registerBag(SessionBagInterface $bag) { @@ -337,10 +299,7 @@ public function registerBag(SessionBagInterface $bag) $this->bags[$bag->getName()] = $bag; } - /** - * {@inheritdoc} - */ - public function getBag(string $name) + public function getBag(string $name): SessionBagInterface { if (!isset($this->bags[$name])) { throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); @@ -355,29 +314,26 @@ public function getBag(string $name) return $this->bags[$name]; } - public function setMetadataBag(MetadataBag $metaBag = null) + /** + * @return void + */ + public function setMetadataBag(?MetadataBag $metaBag = null) { - if (null === $metaBag) { - $metaBag = new MetadataBag(); + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); } - - $this->metadataBag = $metaBag; + $this->metadataBag = $metaBag ?? new MetadataBag(); } /** * Gets the MetadataBag. - * - * @return MetadataBag */ - public function getMetadataBag() + public function getMetadataBag(): MetadataBag { return $this->metadataBag; } - /** - * {@inheritdoc} - */ - public function isStarted() + public function isStarted(): bool { return $this->started; } @@ -391,6 +347,8 @@ public function isStarted() * @param array $options Session ini directives [key => value] * * @see https://php.net/session.configuration + * + * @return void */ public function setOptions(array $options) { @@ -404,31 +362,16 @@ public function setOptions(array $options) 'gc_divisor', 'gc_maxlifetime', 'gc_probability', 'lazy_write', 'name', 'referer_check', 'serialize_handler', 'use_strict_mode', 'use_cookies', - 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', - 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', - 'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags', + 'use_only_cookies', 'use_trans_sid', 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', ]); foreach ($options as $key => $value) { if (isset($validOptions[$key])) { - if (str_starts_with($key, 'upload_progress.')) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. The settings prefixed with "session.upload_progress." can not be changed at runtime.', $key); - continue; - } - if ('url_rewriter.tags' === $key) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. Use "trans_sid_tags" instead.', $key); - } - if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) { - // PHP < 7.3 does not support same_site cookies. We will emulate it in - // the start() method instead. - $this->emulateSameSite = $value; - continue; - } if ('cookie_secure' === $key && 'auto' === $value) { continue; } - ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value); + ini_set('session.'.$key, $value); } } } @@ -449,17 +392,14 @@ public function setOptions(array $options) * @see https://php.net/sessionhandlerinterface * @see https://php.net/sessionhandler * - * @param AbstractProxy|\SessionHandlerInterface|null $saveHandler + * @return void * * @throws \InvalidArgumentException */ - public function setSaveHandler($saveHandler = null) + public function setSaveHandler(AbstractProxy|\SessionHandlerInterface|null $saveHandler = null) { - if (!$saveHandler instanceof AbstractProxy - && !$saveHandler instanceof \SessionHandlerInterface - && null !== $saveHandler - ) { - throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); } // Wrap $saveHandler in proxy and prevent double wrapping of proxy @@ -486,8 +426,10 @@ public function setSaveHandler($saveHandler = null) * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). * PHP takes the return value from the read() handler, unserializes it * and populates $_SESSION with the result automatically. + * + * @return void */ - protected function loadSession(array &$session = null) + protected function loadSession(?array &$session = null) { if (null === $session) { $session = &$_SESSION; diff --git a/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php b/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php index a7d7411ff..6463a4c1b 100644 --- a/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php +++ b/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; // Help opcache.preload discover always-needed symbols class_exists(NativeSessionStorage::class); @@ -21,15 +22,15 @@ class_exists(NativeSessionStorage::class); */ class NativeSessionStorageFactory implements SessionStorageFactoryInterface { - private $options; - private $handler; - private $metaBag; - private $secure; + private array $options; + private AbstractProxy|\SessionHandlerInterface|null $handler; + private ?MetadataBag $metaBag; + private bool $secure; /** * @see NativeSessionStorage constructor. */ - public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null, bool $secure = false) + public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) { $this->options = $options; $this->handler = $handler; @@ -40,7 +41,7 @@ public function __construct(array $options = [], $handler = null, MetadataBag $m public function createStorage(?Request $request): SessionStorageInterface { $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag); - if ($this->secure && $request && $request->isSecure()) { + if ($this->secure && $request?->isSecure()) { $storage->setOptions(['cookie_secure' => true]); } diff --git a/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php index 72dbef134..4fb26d2a9 100644 --- a/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php +++ b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php @@ -20,10 +20,7 @@ */ class PhpBridgeSessionStorage extends NativeSessionStorage { - /** - * @param AbstractProxy|\SessionHandlerInterface|null $handler - */ - public function __construct($handler = null, MetadataBag $metaBag = null) + public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null) { if (!\extension_loaded('session')) { throw new \LogicException('PHP extension "session" is required.'); @@ -33,10 +30,7 @@ public function __construct($handler = null, MetadataBag $metaBag = null) $this->setSaveHandler($handler); } - /** - * {@inheritdoc} - */ - public function start() + public function start(): bool { if ($this->started) { return true; @@ -48,7 +42,7 @@ public function start() } /** - * {@inheritdoc} + * @return void */ public function clear() { diff --git a/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php index 173ef71de..aa4f800d3 100644 --- a/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php +++ b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; // Help opcache.preload discover always-needed symbols class_exists(PhpBridgeSessionStorage::class); @@ -21,14 +22,11 @@ class_exists(PhpBridgeSessionStorage::class); */ class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface { - private $handler; - private $metaBag; - private $secure; - - /** - * @see PhpBridgeSessionStorage constructor. - */ - public function __construct($handler = null, MetadataBag $metaBag = null, bool $secure = false) + private AbstractProxy|\SessionHandlerInterface|null $handler; + private ?MetadataBag $metaBag; + private bool $secure; + + public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) { $this->handler = $handler; $this->metaBag = $metaBag; @@ -38,7 +36,7 @@ public function __construct($handler = null, MetadataBag $metaBag = null, bool $ public function createStorage(?Request $request): SessionStorageInterface { $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag); - if ($this->secure && $request && $request->isSecure()) { + if ($this->secure && $request?->isSecure()) { $storage->setOptions(['cookie_secure' => true]); } diff --git a/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php b/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php index edd04dff8..2fcd06b10 100644 --- a/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php +++ b/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php @@ -30,50 +30,40 @@ abstract class AbstractProxy /** * Gets the session.save_handler name. - * - * @return string|null */ - public function getSaveHandlerName() + public function getSaveHandlerName(): ?string { return $this->saveHandlerName; } /** * Is this proxy handler and instance of \SessionHandlerInterface. - * - * @return bool */ - public function isSessionHandlerInterface() + public function isSessionHandlerInterface(): bool { return $this instanceof \SessionHandlerInterface; } /** * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. - * - * @return bool */ - public function isWrapper() + public function isWrapper(): bool { return $this->wrapper; } /** * Has a session started? - * - * @return bool */ - public function isActive() + public function isActive(): bool { return \PHP_SESSION_ACTIVE === session_status(); } /** * Gets the session ID. - * - * @return string */ - public function getId() + public function getId(): string { return session_id(); } @@ -81,6 +71,8 @@ public function getId() /** * Sets the session ID. * + * @return void + * * @throws \LogicException */ public function setId(string $id) @@ -94,10 +86,8 @@ public function setId(string $id) /** * Gets the session name. - * - * @return string */ - public function getName() + public function getName(): string { return session_name(); } @@ -105,6 +95,8 @@ public function getName() /** * Sets the session name. * + * @return void + * * @throws \LogicException */ public function setName(string $name) diff --git a/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php b/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php index 0defa4a7a..7bf3f9ff1 100644 --- a/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -27,84 +27,49 @@ public function __construct(\SessionHandlerInterface $handler) $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; } - /** - * @return \SessionHandlerInterface - */ - public function getHandler() + public function getHandler(): \SessionHandlerInterface { return $this->handler; } // \SessionHandlerInterface - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function open($savePath, $sessionName) + public function open(string $savePath, string $sessionName): bool { return $this->handler->open($savePath, $sessionName); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function close() + public function close(): bool { return $this->handler->close(); } - /** - * @return string|false - */ - #[\ReturnTypeWillChange] - public function read($sessionId) + public function read(#[\SensitiveParameter] string $sessionId): string|false { return $this->handler->read($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function write($sessionId, $data) + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler->write($sessionId, $data); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function destroy($sessionId) + public function destroy(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->destroy($sessionId); } - /** - * @return int|false - */ - #[\ReturnTypeWillChange] - public function gc($maxlifetime) + public function gc(int $maxlifetime): int|false { return $this->handler->gc($maxlifetime); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function validateId($sessionId) + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function updateTimestamp($sessionId, $data) + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); } diff --git a/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php b/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php deleted file mode 100644 index d17c60aeb..000000000 --- a/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpFoundation\Session\Storage; - -use Symfony\Component\HttpFoundation\Request; - -/** - * @author Jérémy Derussé - * - * @internal to be removed in Symfony 6 - */ -final class ServiceSessionFactory implements SessionStorageFactoryInterface -{ - private $storage; - - public function __construct(SessionStorageInterface $storage) - { - $this->storage = $storage; - } - - public function createStorage(?Request $request): SessionStorageInterface - { - if ($this->storage instanceof NativeSessionStorage && $request && $request->isSecure()) { - $this->storage->setOptions(['cookie_secure' => true]); - } - - return $this->storage; - } -} diff --git a/symfony/http-foundation/Session/Storage/SessionStorageInterface.php b/symfony/http-foundation/Session/Storage/SessionStorageInterface.php index 705374552..7865135b0 100644 --- a/symfony/http-foundation/Session/Storage/SessionStorageInterface.php +++ b/symfony/http-foundation/Session/Storage/SessionStorageInterface.php @@ -24,40 +24,36 @@ interface SessionStorageInterface /** * Starts the session. * - * @return bool - * * @throws \RuntimeException if something goes wrong starting the session */ - public function start(); + public function start(): bool; /** * Checks if the session is started. - * - * @return bool */ - public function isStarted(); + public function isStarted(): bool; /** * Returns the session ID. - * - * @return string */ - public function getId(); + public function getId(): string; /** * Sets the session ID. + * + * @return void */ public function setId(string $id); /** * Returns the session name. - * - * @return string */ - public function getName(); + public function getName(): string; /** * Sets the session name. + * + * @return void */ public function setName(string $name); @@ -86,11 +82,9 @@ public function setName(string $name); * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. * - * @return bool - * * @throws \RuntimeException If an error occurs while regenerating this storage */ - public function regenerate(bool $destroy = false, int $lifetime = null); + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool; /** * Force the session to be saved and closed. @@ -100,6 +94,8 @@ public function regenerate(bool $destroy = false, int $lifetime = null); * a real PHP session would interfere with testing, in which case * it should actually persist the session data if required. * + * @return void + * * @throws \RuntimeException if the session is saved without being started, or if the session * is already closed */ @@ -107,25 +103,24 @@ public function save(); /** * Clear all session data in memory. + * + * @return void */ public function clear(); /** * Gets a SessionBagInterface by name. * - * @return SessionBagInterface - * * @throws \InvalidArgumentException If the bag does not exist */ - public function getBag(string $name); + public function getBag(string $name): SessionBagInterface; /** * Registers a SessionBagInterface for use. + * + * @return void */ public function registerBag(SessionBagInterface $bag); - /** - * @return MetadataBag - */ - public function getMetadataBag(); + public function getMetadataBag(): MetadataBag; } diff --git a/symfony/http-foundation/StreamedJsonResponse.php b/symfony/http-foundation/StreamedJsonResponse.php new file mode 100644 index 000000000..5b20ce910 --- /dev/null +++ b/symfony/http-foundation/StreamedJsonResponse.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * StreamedJsonResponse represents a streamed HTTP response for JSON. + * + * A StreamedJsonResponse uses a structure and generics to create an + * efficient resource-saving JSON response. + * + * It is recommended to use flush() function after a specific number of items to directly stream the data. + * + * @see flush() + * + * @author Alexander Schranz + * + * Example usage: + * + * function loadArticles(): \Generator + * // some streamed loading + * yield ['title' => 'Article 1']; + * yield ['title' => 'Article 2']; + * yield ['title' => 'Article 3']; + * // recommended to use flush() after every specific number of items + * }), + * + * $response = new StreamedJsonResponse( + * // json structure with generators in which will be streamed + * [ + * '_embedded' => [ + * 'articles' => loadArticles(), // any generator which you want to stream as list of data + * ], + * ], + * ); + */ +class StreamedJsonResponse extends StreamedResponse +{ + private const PLACEHOLDER = '__symfony_json__'; + + /** + * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data or a Generator + * @param int $status The HTTP status code (200 "OK" by default) + * @param array $headers An array of HTTP headers + * @param int $encodingOptions Flags for the json_encode() function + */ + public function __construct( + private readonly iterable $data, + int $status = 200, + array $headers = [], + private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, + ) { + parent::__construct($this->stream(...), $status, $headers); + + if (!$this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/json'); + } + } + + private function stream(): void + { + $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; + $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; + + $this->streamData($this->data, $jsonEncodingOptions, $keyEncodingOptions); + } + + private function streamData(mixed $data, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + if (\is_array($data)) { + $this->streamArray($data, $jsonEncodingOptions, $keyEncodingOptions); + + return; + } + + if (is_iterable($data) && !$data instanceof \JsonSerializable) { + $this->streamIterable($data, $jsonEncodingOptions, $keyEncodingOptions); + + return; + } + + echo json_encode($data, $jsonEncodingOptions); + } + + private function streamArray(array $data, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + $generators = []; + + array_walk_recursive($data, function (&$item, $key) use (&$generators) { + if (self::PLACEHOLDER === $key) { + // if the placeholder is already in the structure it should be replaced with a new one that explode + // works like expected for the structure + $generators[] = $key; + } + + // generators should be used but for better DX all kind of Traversable and objects are supported + if (\is_object($item)) { + $generators[] = $item; + $item = self::PLACEHOLDER; + } elseif (self::PLACEHOLDER === $item) { + // if the placeholder is already in the structure it should be replaced with a new one that explode + // works like expected for the structure + $generators[] = $item; + } + }); + + $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($data, $jsonEncodingOptions)); + + foreach ($generators as $index => $generator) { + // send first and between parts of the structure + echo $jsonParts[$index]; + + $this->streamData($generator, $jsonEncodingOptions, $keyEncodingOptions); + } + + // send last part of the structure + echo $jsonParts[array_key_last($jsonParts)]; + } + + private function streamIterable(iterable $iterable, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + $isFirstItem = true; + $startTag = '['; + + foreach ($iterable as $key => $item) { + if ($isFirstItem) { + $isFirstItem = false; + // depending on the first elements key the generator is detected as a list or map + // we can not check for a whole list or map because that would hurt the performance + // of the streamed response which is the main goal of this response class + if (0 !== $key) { + $startTag = '{'; + } + + echo $startTag; + } else { + // if not first element of the generic, a separator is required between the elements + echo ','; + } + + if ('{' === $startTag) { + echo json_encode((string) $key, $keyEncodingOptions).':'; + } + + $this->streamData($item, $jsonEncodingOptions, $keyEncodingOptions); + } + + if ($isFirstItem) { // indicates that the generator was empty + echo '['; + } + + echo '[' === $startTag ? ']' : '}'; + } +} diff --git a/symfony/http-foundation/StreamedResponse.php b/symfony/http-foundation/StreamedResponse.php index 0599bd1e4..0ab88e098 100644 --- a/symfony/http-foundation/StreamedResponse.php +++ b/symfony/http-foundation/StreamedResponse.php @@ -28,9 +28,12 @@ class StreamedResponse extends Response { protected $callback; protected $streamed; - private $headersSent; + private bool $headersSent; - public function __construct(callable $callback = null, int $status = 200, array $headers = []) + /** + * @param int $status The HTTP status code (200 "OK" by default) + */ + public function __construct(?callable $callback = null, int $status = 200, array $headers = []) { parent::__construct(null, $status, $headers); @@ -42,59 +45,53 @@ public function __construct(callable $callback = null, int $status = 200, array } /** - * Factory method for chainability. - * - * @param callable|null $callback A valid PHP callback or null to set it later - * - * @return static + * Sets the PHP callback associated with this Response. * - * @deprecated since Symfony 5.1, use __construct() instead. + * @return $this */ - public static function create($callback = null, int $status = 200, array $headers = []) + public function setCallback(callable $callback): static { - trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + $this->callback = $callback(...); - return new static($callback, $status, $headers); + return $this; } - /** - * Sets the PHP callback associated with this Response. - * - * @return $this - */ - public function setCallback(callable $callback) + public function getCallback(): ?\Closure { - $this->callback = $callback; + if (!isset($this->callback)) { + return null; + } - return $this; + return ($this->callback)(...); } /** - * {@inheritdoc} - * * This method only sends the headers once. * + * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null + * * @return $this */ - public function sendHeaders() + public function sendHeaders(/* int $statusCode = null */): static { if ($this->headersSent) { return $this; } - $this->headersSent = true; + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + if ($statusCode < 100 || $statusCode >= 200) { + $this->headersSent = true; + } - return parent::sendHeaders(); + return parent::sendHeaders($statusCode); } /** - * {@inheritdoc} - * * This method only sends the content once. * * @return $this */ - public function sendContent() + public function sendContent(): static { if ($this->streamed) { return $this; @@ -102,8 +99,8 @@ public function sendContent() $this->streamed = true; - if (null === $this->callback) { - throw new \LogicException('The Response callback must not be null.'); + if (!isset($this->callback)) { + throw new \LogicException('The Response callback must be set.'); } ($this->callback)(); @@ -112,13 +109,11 @@ public function sendContent() } /** - * {@inheritdoc} - * * @return $this * * @throws \LogicException when the content is not null */ - public function setContent(?string $content) + public function setContent(?string $content): static { if (null !== $content) { throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); @@ -129,10 +124,7 @@ public function setContent(?string $content) return $this; } - /** - * {@inheritdoc} - */ - public function getContent() + public function getContent(): string|false { return false; } diff --git a/symfony/http-foundation/UriSigner.php b/symfony/http-foundation/UriSigner.php new file mode 100644 index 000000000..b04987724 --- /dev/null +++ b/symfony/http-foundation/UriSigner.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * @author Fabien Potencier + */ +class UriSigner +{ + private string $secret; + private string $parameter; + + /** + * @param string $parameter Query string parameter to use + */ + public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash') + { + if (!$secret) { + throw new \InvalidArgumentException('A non-empty secret is required.'); + } + + $this->secret = $secret; + $this->parameter = $parameter; + } + + /** + * Signs a URI. + * + * The given URI is signed by adding the query string parameter + * which value depends on the URI and the secret. + */ + public function sign(string $uri): string + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + $uri = $this->buildUrl($url, $params); + $params[$this->parameter] = $this->computeHash($uri); + + return $this->buildUrl($url, $params); + } + + /** + * Checks that a URI contains the correct hash. + */ + public function check(string $uri): bool + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + if (empty($params[$this->parameter])) { + return false; + } + + $hash = $params[$this->parameter]; + unset($params[$this->parameter]); + + return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash); + } + + public function checkRequest(Request $request): bool + { + $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; + + // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) + return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs); + } + + private function computeHash(string $uri): string + { + return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); + } + + private function buildUrl(array $url, array $params = []): string + { + ksort($params, \SORT_STRING); + $url['query'] = http_build_query($params, '', '&'); + + $scheme = isset($url['scheme']) ? $url['scheme'].'://' : ''; + $host = $url['host'] ?? ''; + $port = isset($url['port']) ? ':'.$url['port'] : ''; + $user = $url['user'] ?? ''; + $pass = isset($url['pass']) ? ':'.$url['pass'] : ''; + $pass = ($user || $pass) ? "$pass@" : ''; + $path = $url['path'] ?? ''; + $query = $url['query'] ? '?'.$url['query'] : ''; + $fragment = isset($url['fragment']) ? '#'.$url['fragment'] : ''; + + return $scheme.$user.$pass.$host.$port.$path.$query.$fragment; + } +} + +if (!class_exists(\Symfony\Component\HttpKernel\UriSigner::class, false)) { + class_alias(UriSigner::class, \Symfony\Component\HttpKernel\UriSigner::class); +} diff --git a/symfony/http-foundation/UrlHelper.php b/symfony/http-foundation/UrlHelper.php index 90659947d..f971cf662 100644 --- a/symfony/http-foundation/UrlHelper.php +++ b/symfony/http-foundation/UrlHelper.php @@ -21,25 +21,15 @@ */ final class UrlHelper { - private $requestStack; - private $requestContext; - - /** - * @param RequestContextAwareInterface|RequestContext|null $requestContext - */ - public function __construct(RequestStack $requestStack, $requestContext = null) - { - if (null !== $requestContext && !$requestContext instanceof RequestContext && !$requestContext instanceof RequestContextAwareInterface) { - throw new \TypeError(__METHOD__.': Argument #2 ($requestContext) must of type Symfony\Component\Routing\RequestContextAwareInterface|Symfony\Component\Routing\RequestContext|null, '.get_debug_type($requestContext).' given.'); - } - - $this->requestStack = $requestStack; - $this->requestContext = $requestContext; + public function __construct( + private RequestStack $requestStack, + private RequestContextAwareInterface|RequestContext|null $requestContext = null, + ) { } public function getAbsoluteUrl(string $path): string { - if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { + if (str_contains($path, '://') || str_starts_with($path, '//')) { return $path; } @@ -68,7 +58,7 @@ public function getAbsoluteUrl(string $path): string public function getRelativePath(string $path): string { - if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { + if (str_contains($path, '://') || str_starts_with($path, '//')) { return $path; } diff --git a/symfony/mailer/Command/MailerTestCommand.php b/symfony/mailer/Command/MailerTestCommand.php new file mode 100644 index 000000000..bfc2779e3 --- /dev/null +++ b/symfony/mailer/Command/MailerTestCommand.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Email; + +/** + * A console command to test Mailer transports. + */ +#[AsCommand(name: 'mailer:test', description: 'Test Mailer transports by sending an email')] +final class MailerTestCommand extends Command +{ + public function __construct(private TransportInterface $transport) + { + $this->transport = $transport; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message') + ->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'from@example.org') + ->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'The subject of the message', 'Testing transport') + ->addOption('body', null, InputOption::VALUE_OPTIONAL, 'The body of the message', 'Testing body') + ->addOption('transport', null, InputOption::VALUE_OPTIONAL, 'The transport to be used') + ->setHelp(<<<'EOF' +The %command.name% command tests a Mailer transport by sending a simple email message: + +php %command.full_name% to@example.com + +You can also specify a specific transport: + + php %command.full_name% to@example.com --transport=transport_name + +Note that this command bypasses the Messenger bus if configured. + +EOF + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $message = (new Email()) + ->to($input->getArgument('to')) + ->from($input->getOption('from')) + ->subject($input->getOption('subject')) + ->text($input->getOption('body')) + ; + if ($transport = $input->getOption('transport')) { + $message->getHeaders()->addTextHeader('X-Transport', $transport); + } + + $this->transport->send($message); + + return 0; + } +} diff --git a/symfony/mailer/DataCollector/MessageDataCollector.php b/symfony/mailer/DataCollector/MessageDataCollector.php index 07f77b27b..5c9ac3544 100644 --- a/symfony/mailer/DataCollector/MessageDataCollector.php +++ b/symfony/mailer/DataCollector/MessageDataCollector.php @@ -22,17 +22,14 @@ */ final class MessageDataCollector extends DataCollector { - private $events; + private MessageEvents $events; public function __construct(MessageLoggerListener $logger) { $this->events = $logger->getEvents(); } - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, \Throwable $exception = null) + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->data['events'] = $this->events; } @@ -50,17 +47,11 @@ public function base64Encode(string $data): string return base64_encode($data); } - /** - * {@inheritdoc} - */ - public function reset() + public function reset(): void { $this->data = []; } - /** - * {@inheritdoc} - */ public function getName(): string { return 'mailer'; diff --git a/symfony/mailer/DelayedEnvelope.php b/symfony/mailer/DelayedEnvelope.php index b78b6f294..7458db2f0 100644 --- a/symfony/mailer/DelayedEnvelope.php +++ b/symfony/mailer/DelayedEnvelope.php @@ -23,9 +23,9 @@ */ final class DelayedEnvelope extends Envelope { - private $senderSet = false; - private $recipientsSet = false; - private $message; + private bool $senderSet = false; + private bool $recipientsSet = false; + private Message $message; public function __construct(Message $message) { @@ -52,7 +52,7 @@ public function setRecipients(array $recipients): void { parent::setRecipients($recipients); - $this->recipientsSet = parent::getRecipients(); + $this->recipientsSet = (bool) parent::getRecipients(); } /** diff --git a/symfony/mailer/Envelope.php b/symfony/mailer/Envelope.php index 97c8d85c5..0a4af2eda 100644 --- a/symfony/mailer/Envelope.php +++ b/symfony/mailer/Envelope.php @@ -21,8 +21,8 @@ */ class Envelope { - private $sender; - private $recipients = []; + private Address $sender; + private array $recipients = []; /** * @param Address[] $recipients @@ -35,7 +35,7 @@ public function __construct(Address $sender, array $recipients) public static function create(RawMessage $message): self { - if (RawMessage::class === \get_class($message)) { + if (RawMessage::class === $message::class) { throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.'); } diff --git a/symfony/mailer/Event/FailedMessageEvent.php b/symfony/mailer/Event/FailedMessageEvent.php new file mode 100644 index 000000000..4808c659d --- /dev/null +++ b/symfony/mailer/Event/FailedMessageEvent.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author Fabien Potencier + */ +final class FailedMessageEvent extends Event +{ + public function __construct( + private RawMessage $message, + private \Throwable $error, + ) { + } + + public function getMessage(): RawMessage + { + return $this->message; + } + + public function getError(): \Throwable + { + return $this->error; + } +} diff --git a/symfony/mailer/Event/MessageEvent.php b/symfony/mailer/Event/MessageEvent.php index d6b7894ea..f00fdd52c 100644 --- a/symfony/mailer/Event/MessageEvent.php +++ b/symfony/mailer/Event/MessageEvent.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Mailer\Event; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Messenger\Stamp\StampInterface; use Symfony\Component\Mime\RawMessage; use Symfony\Contracts\EventDispatcher\Event; @@ -22,10 +24,14 @@ */ final class MessageEvent extends Event { - private $message; - private $envelope; - private $transport; - private $queued; + private RawMessage $message; + private Envelope $envelope; + private string $transport; + private bool $queued; + private bool $rejected = false; + + /** @var StampInterface[] */ + private array $stamps = []; public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false) { @@ -64,4 +70,36 @@ public function isQueued(): bool { return $this->queued; } + + public function isRejected(): bool + { + return $this->rejected; + } + + public function reject(): void + { + $this->rejected = true; + $this->stopPropagation(); + } + + public function addStamp(StampInterface $stamp): void + { + if (!$this->queued) { + throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__)); + } + + $this->stamps[] = $stamp; + } + + /** + * @return StampInterface[] + */ + public function getStamps(): array + { + if (!$this->queued) { + throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__)); + } + + return $this->stamps; + } } diff --git a/symfony/mailer/Event/MessageEvents.php b/symfony/mailer/Event/MessageEvents.php index b5266493c..2b438e382 100644 --- a/symfony/mailer/Event/MessageEvents.php +++ b/symfony/mailer/Event/MessageEvents.php @@ -18,8 +18,15 @@ */ class MessageEvents { - private $events = []; - private $transports = []; + /** + * @var MessageEvent[] + */ + private array $events = []; + + /** + * @var array + */ + private array $transports = []; public function add(MessageEvent $event): void { @@ -35,7 +42,7 @@ public function getTransports(): array /** * @return MessageEvent[] */ - public function getEvents(string $name = null): array + public function getEvents(?string $name = null): array { if (null === $name) { return $this->events; @@ -54,7 +61,7 @@ public function getEvents(string $name = null): array /** * @return RawMessage[] */ - public function getMessages(string $name = null): array + public function getMessages(?string $name = null): array { $events = $this->getEvents($name); $messages = []; diff --git a/symfony/mailer/Event/SentMessageEvent.php b/symfony/mailer/Event/SentMessageEvent.php new file mode 100644 index 000000000..a412a9f46 --- /dev/null +++ b/symfony/mailer/Event/SentMessageEvent.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mailer\SentMessage; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author Fabien Potencier + */ +final class SentMessageEvent extends Event +{ + public function __construct(private SentMessage $message) + { + } + + public function getMessage(): SentMessage + { + return $this->message; + } +} diff --git a/symfony/mailer/EventListener/EnvelopeListener.php b/symfony/mailer/EventListener/EnvelopeListener.php index b2980bc5c..db9dd723a 100644 --- a/symfony/mailer/EventListener/EnvelopeListener.php +++ b/symfony/mailer/EventListener/EnvelopeListener.php @@ -23,14 +23,17 @@ */ class EnvelopeListener implements EventSubscriberInterface { - private $sender; - private $recipients; + private ?Address $sender = null; + + /** + * @var Address[]|null + */ + private ?array $recipients = null; /** - * @param Address|string $sender * @param array $recipients */ - public function __construct($sender = null, array $recipients = null) + public function __construct(Address|string|null $sender = null, ?array $recipients = null) { if (null !== $sender) { $this->sender = Address::create($sender); @@ -58,7 +61,7 @@ public function onMessage(MessageEvent $event): void } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // should be the last one to allow header changes by other listeners first diff --git a/symfony/mailer/EventListener/MessageListener.php b/symfony/mailer/EventListener/MessageListener.php index f23c69d91..ec822d9c6 100644 --- a/symfony/mailer/EventListener/MessageListener.php +++ b/symfony/mailer/EventListener/MessageListener.php @@ -39,11 +39,11 @@ class MessageListener implements EventSubscriberInterface 'bcc' => self::HEADER_ADD, ]; - private $headers; - private $headerRules = []; - private $renderer; + private ?Headers $headers; + private array $headerRules = []; + private ?BodyRendererInterface $renderer; - public function __construct(Headers $headers = null, BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) + public function __construct(?Headers $headers = null, ?BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) { $this->headers = $headers; $this->renderer = $renderer; @@ -125,7 +125,7 @@ private function renderMessage(Message $message): void $this->renderer->render($message); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ MessageEvent::class => 'onMessage', diff --git a/symfony/mailer/EventListener/MessageLoggerListener.php b/symfony/mailer/EventListener/MessageLoggerListener.php index 093bf2bb9..6c5605fa8 100644 --- a/symfony/mailer/EventListener/MessageLoggerListener.php +++ b/symfony/mailer/EventListener/MessageLoggerListener.php @@ -23,7 +23,7 @@ */ class MessageLoggerListener implements EventSubscriberInterface, ResetInterface { - private $events; + private MessageEvents $events; public function __construct() { @@ -31,7 +31,7 @@ public function __construct() } /** - * {@inheritdoc} + * @return void */ public function reset() { @@ -48,7 +48,7 @@ public function getEvents(): MessageEvents return $this->events; } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ MessageEvent::class => ['onMessage', -255], diff --git a/symfony/mailer/EventListener/MessengerTransportListener.php b/symfony/mailer/EventListener/MessengerTransportListener.php new file mode 100644 index 000000000..8cf8cc800 --- /dev/null +++ b/symfony/mailer/EventListener/MessengerTransportListener.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Messenger\Stamp\TransportNamesStamp; +use Symfony\Component\Mime\Message; + +/** + * Allows messages to be sent to specific Messenger transports via the "X-Bus-Transport" MIME header. + * + * @author Fabien Potencier + */ +final class MessengerTransportListener implements EventSubscriberInterface +{ + public function onMessage(MessageEvent $event): void + { + if (!$event->isQueued()) { + return; + } + + $message = $event->getMessage(); + if (!$message instanceof Message || !$message->getHeaders()->has('X-Bus-Transport')) { + return; + } + + $names = $message->getHeaders()->get('X-Bus-Transport')->getBody(); + $names = array_map('trim', explode(',', $names)); + $event->addStamp(new TransportNamesStamp($names)); + $message->getHeaders()->remove('X-Bus-Transport'); + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => 'onMessage', + ]; + } +} diff --git a/symfony/mailer/Exception/HttpTransportException.php b/symfony/mailer/Exception/HttpTransportException.php index c72eb6cf6..ad10910f3 100644 --- a/symfony/mailer/Exception/HttpTransportException.php +++ b/symfony/mailer/Exception/HttpTransportException.php @@ -18,16 +18,10 @@ */ class HttpTransportException extends TransportException { - private $response; + private ResponseInterface $response; - public function __construct(?string $message, ResponseInterface $response, int $code = 0, \Throwable $previous = null) + public function __construct(string $message, ResponseInterface $response, int $code = 0, ?\Throwable $previous = null) { - if (null === $message) { - trigger_deprecation('symfony/mailer', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - $message = ''; - } - parent::__construct($message, $code, $previous); $this->response = $response; diff --git a/symfony/mailer/Exception/TransportException.php b/symfony/mailer/Exception/TransportException.php index dfad0c45f..ea538d6fa 100644 --- a/symfony/mailer/Exception/TransportException.php +++ b/symfony/mailer/Exception/TransportException.php @@ -16,7 +16,7 @@ */ class TransportException extends RuntimeException implements TransportExceptionInterface { - private $debug = ''; + private string $debug = ''; public function getDebug(): string { diff --git a/symfony/mailer/Exception/UnexpectedResponseException.php b/symfony/mailer/Exception/UnexpectedResponseException.php new file mode 100644 index 000000000..e779df122 --- /dev/null +++ b/symfony/mailer/Exception/UnexpectedResponseException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +class UnexpectedResponseException extends TransportException +{ +} diff --git a/symfony/mailer/Exception/UnsupportedSchemeException.php b/symfony/mailer/Exception/UnsupportedSchemeException.php index e47a129dc..257fd47db 100644 --- a/symfony/mailer/Exception/UnsupportedSchemeException.php +++ b/symfony/mailer/Exception/UnsupportedSchemeException.php @@ -20,10 +20,22 @@ class UnsupportedSchemeException extends LogicException { private const SCHEME_TO_PACKAGE_MAP = [ + 'brevo' => [ + 'class' => Bridge\Brevo\Transport\BrevoTransportFactory::class, + 'package' => 'symfony/brevo-mailer', + ], 'gmail' => [ 'class' => Bridge\Google\Transport\GmailTransportFactory::class, 'package' => 'symfony/google-mailer', ], + 'infobip' => [ + 'class' => Bridge\Infobip\Transport\InfobipTransportFactory::class, + 'package' => 'symfony/infobip-mailer', + ], + 'mailersend' => [ + 'class' => Bridge\MailerSend\Transport\MailerSendTransportFactory::class, + 'package' => 'symfony/mailersend-mailer', + ], 'mailgun' => [ 'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class, 'package' => 'symfony/mailgun-mailer', @@ -32,6 +44,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, 'package' => 'symfony/mailjet-mailer', ], + 'mailpace' => [ + 'class' => Bridge\MailPace\Transport\MailPaceTransportFactory::class, + 'package' => 'symfony/mail-pace-mailer', + ], 'mandrill' => [ 'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class, 'package' => 'symfony/mailchimp-mailer', @@ -44,6 +60,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class, 'package' => 'symfony/postmark-mailer', ], + 'scaleway' => [ + 'class' => Bridge\Scaleway\Transport\ScalewayTransportFactory::class, + 'package' => 'symfony/scaleway-mailer', + ], 'sendgrid' => [ 'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class, 'package' => 'symfony/sendgrid-mailer', @@ -58,7 +78,7 @@ class UnsupportedSchemeException extends LogicException ], ]; - public function __construct(Dsn $dsn, string $name = null, array $supported = []) + public function __construct(Dsn $dsn, ?string $name = null, array $supported = []) { $provider = $dsn->getScheme(); if (false !== $pos = strpos($provider, '+')) { @@ -66,7 +86,7 @@ public function __construct(Dsn $dsn, string $name = null, array $supported = [] } $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; if ($package && !class_exists($package['class'])) { - parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed; try running "composer require %s".', $provider, $package['package'])); + parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $provider, $package['package'])); return; } diff --git a/symfony/mailer/Header/MetadataHeader.php b/symfony/mailer/Header/MetadataHeader.php index d56acb16b..d6ee5440d 100644 --- a/symfony/mailer/Header/MetadataHeader.php +++ b/symfony/mailer/Header/MetadataHeader.php @@ -18,7 +18,7 @@ */ final class MetadataHeader extends UnstructuredHeader { - private $key; + private string $key; public function __construct(string $key, string $value) { diff --git a/symfony/mailer/Mailer.php b/symfony/mailer/Mailer.php index cbdcdf296..6df99091a 100644 --- a/symfony/mailer/Mailer.php +++ b/symfony/mailer/Mailer.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Mailer; use Psr\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Messenger\SendEmailMessage; @@ -21,25 +19,24 @@ use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Mime\RawMessage; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface; /** * @author Fabien Potencier */ final class Mailer implements MailerInterface { - private $transport; - private $bus; - private $dispatcher; + private TransportInterface $transport; + private ?MessageBusInterface $bus; + private ?EventDispatcherInterface $dispatcher; - public function __construct(TransportInterface $transport, MessageBusInterface $bus = null, EventDispatcherInterface $dispatcher = null) + public function __construct(TransportInterface $transport, ?MessageBusInterface $bus = null, ?EventDispatcherInterface $dispatcher = null) { $this->transport = $transport; $this->bus = $bus; - $this->dispatcher = class_exists(Event::class) && $dispatcher instanceof SymfonyEventDispatcherInterface ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher; + $this->dispatcher = $dispatcher; } - public function send(RawMessage $message, Envelope $envelope = null): void + public function send(RawMessage $message, ?Envelope $envelope = null): void { if (null === $this->bus) { $this->transport->send($message, $envelope); @@ -47,6 +44,7 @@ public function send(RawMessage $message, Envelope $envelope = null): void return; } + $stamps = []; if (null !== $this->dispatcher) { // The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let // listeners do something before a message is sent to the queue. @@ -57,12 +55,17 @@ public function send(RawMessage $message, Envelope $envelope = null): void $clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage); $event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true); $this->dispatcher->dispatch($event); + $stamps = $event->getStamps(); + + if ($event->isRejected()) { + return; + } } try { - $this->bus->dispatch(new SendEmailMessage($message, $envelope)); + $this->bus->dispatch(new SendEmailMessage($message, $envelope), $stamps); } catch (HandlerFailedException $e) { - foreach ($e->getNestedExceptions() as $nested) { + foreach ($e->getWrappedExceptions() as $nested) { if ($nested instanceof TransportExceptionInterface) { throw $nested; } diff --git a/symfony/mailer/MailerInterface.php b/symfony/mailer/MailerInterface.php index eb44cf640..ebac4b53e 100644 --- a/symfony/mailer/MailerInterface.php +++ b/symfony/mailer/MailerInterface.php @@ -15,7 +15,7 @@ use Symfony\Component\Mime\RawMessage; /** - * Interface for mailers able to send emails synchronous and/or asynchronous. + * Interface for mailers able to send emails synchronously and/or asynchronously. * * Implementations must support synchronous and asynchronous sending. * @@ -26,5 +26,5 @@ interface MailerInterface /** * @throws TransportExceptionInterface */ - public function send(RawMessage $message, Envelope $envelope = null): void; + public function send(RawMessage $message, ?Envelope $envelope = null): void; } diff --git a/symfony/mailer/Messenger/MessageHandler.php b/symfony/mailer/Messenger/MessageHandler.php index fefae9d0c..f8fb14fce 100644 --- a/symfony/mailer/Messenger/MessageHandler.php +++ b/symfony/mailer/Messenger/MessageHandler.php @@ -19,7 +19,7 @@ */ class MessageHandler { - private $transport; + private TransportInterface $transport; public function __construct(TransportInterface $transport) { diff --git a/symfony/mailer/Messenger/SendEmailMessage.php b/symfony/mailer/Messenger/SendEmailMessage.php index b06ac839c..18bd50620 100644 --- a/symfony/mailer/Messenger/SendEmailMessage.php +++ b/symfony/mailer/Messenger/SendEmailMessage.php @@ -19,10 +19,10 @@ */ class SendEmailMessage { - private $message; - private $envelope; + private RawMessage $message; + private ?Envelope $envelope; - public function __construct(RawMessage $message, Envelope $envelope = null) + public function __construct(RawMessage $message, ?Envelope $envelope = null) { $this->message = $message; $this->envelope = $envelope; diff --git a/symfony/mailer/SentMessage.php b/symfony/mailer/SentMessage.php index 1a12d078e..be8471180 100644 --- a/symfony/mailer/SentMessage.php +++ b/symfony/mailer/SentMessage.php @@ -19,11 +19,11 @@ */ class SentMessage { - private $original; - private $raw; - private $envelope; - private $messageId; - private $debug = ''; + private RawMessage $original; + private RawMessage $raw; + private Envelope $envelope; + private string $messageId; + private string $debug = ''; /** * @internal diff --git a/symfony/mailer/Transport.php b/symfony/mailer/Transport.php index c2b813f94..599f4fbce 100644 --- a/symfony/mailer/Transport.php +++ b/symfony/mailer/Transport.php @@ -14,12 +14,17 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; use Symfony\Component\Mailer\Exception\InvalidArgumentException; @@ -39,52 +44,37 @@ /** * @author Fabien Potencier * @author Konstantin Myakshin - * - * @final since Symfony 5.4 */ -class Transport +final class Transport { private const FACTORY_CLASSES = [ + BrevoTransportFactory::class, GmailTransportFactory::class, + InfobipTransportFactory::class, + MailerSendTransportFactory::class, MailgunTransportFactory::class, MailjetTransportFactory::class, + MailPaceTransportFactory::class, MandrillTransportFactory::class, OhMySmtpTransportFactory::class, PostmarkTransportFactory::class, + ScalewayTransportFactory::class, SendgridTransportFactory::class, SendinblueTransportFactory::class, SesTransportFactory::class, ]; - private $factories; + private iterable $factories; - /** - * @param EventDispatcherInterface|null $dispatcher - * @param HttpClientInterface|null $client - * @param LoggerInterface|null $logger - */ - public static function fromDsn(string $dsn/* , EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null */): TransportInterface + public static function fromDsn(#[\SensitiveParameter] string $dsn, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface { - $dispatcher = 2 <= \func_num_args() ? func_get_arg(1) : null; - $client = 3 <= \func_num_args() ? func_get_arg(2) : null; - $logger = 4 <= \func_num_args() ? func_get_arg(3) : null; - $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromString($dsn); } - /** - * @param EventDispatcherInterface|null $dispatcher - * @param HttpClientInterface|null $client - * @param LoggerInterface|null $logger - */ - public static function fromDsns(array $dsns/* , EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null */): TransportInterface + public static function fromDsns(#[\SensitiveParameter] array $dsns, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface { - $dispatcher = 2 <= \func_num_args() ? func_get_arg(1) : null; - $client = 3 <= \func_num_args() ? func_get_arg(2) : null; - $logger = 4 <= \func_num_args() ? func_get_arg(3) : null; - $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromStrings($dsns); @@ -98,7 +88,7 @@ public function __construct(iterable $factories) $this->factories = $factories; } - public function fromStrings(array $dsns): Transports + public function fromStrings(#[\SensitiveParameter] array $dsns): Transports { $transports = []; foreach ($dsns as $name => $dsn) { @@ -108,17 +98,17 @@ public function fromStrings(array $dsns): Transports return new Transports($transports); } - public function fromString(string $dsn): TransportInterface + public function fromString(#[\SensitiveParameter] string $dsn): TransportInterface { [$transport, $offset] = $this->parseDsn($dsn); if ($offset !== \strlen($dsn)) { - throw new InvalidArgumentException(sprintf('The DSN has some garbage at the end: "%s".', substr($dsn, $offset))); + throw new InvalidArgumentException('The mailer DSN has some garbage at the end.'); } return $transport; } - private function parseDsn(string $dsn, int $offset = 0): array + private function parseDsn(#[\SensitiveParameter] string $dsn, int $offset = 0): array { static $keywords = [ 'failover' => FailoverTransport::class, @@ -177,18 +167,10 @@ public function fromDsnObject(Dsn $dsn): TransportInterface } /** - * @param EventDispatcherInterface|null $dispatcher - * @param HttpClientInterface|null $client - * @param LoggerInterface|null $logger - * * @return \Traversable */ - public static function getDefaultFactories(/* EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null */): iterable + public static function getDefaultFactories(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): \Traversable { - $dispatcher = 1 <= \func_num_args() ? func_get_arg(0) : null; - $client = 2 <= \func_num_args() ? func_get_arg(1) : null; - $logger = 3 <= \func_num_args() ? func_get_arg(2) : null; - foreach (self::FACTORY_CLASSES as $factoryClass) { if (class_exists($factoryClass)) { yield new $factoryClass($dispatcher, $client, $logger); diff --git a/symfony/mailer/Transport/AbstractApiTransport.php b/symfony/mailer/Transport/AbstractApiTransport.php index 392484dd6..040745eed 100644 --- a/symfony/mailer/Transport/AbstractApiTransport.php +++ b/symfony/mailer/Transport/AbstractApiTransport.php @@ -37,10 +37,11 @@ protected function doSendHttp(SentMessage $message): ResponseInterface return $this->doSendApi($message, $email, $message->getEnvelope()); } + /** + * @return Address[] + */ protected function getRecipients(Email $email, Envelope $envelope): array { - return array_filter($envelope->getRecipients(), function (Address $address) use ($email) { - return false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true); - }); + return array_filter($envelope->getRecipients(), fn (Address $address) => false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true)); } } diff --git a/symfony/mailer/Transport/AbstractHttpTransport.php b/symfony/mailer/Transport/AbstractHttpTransport.php index 2317a0da5..96b7ec009 100644 --- a/symfony/mailer/Transport/AbstractHttpTransport.php +++ b/symfony/mailer/Transport/AbstractHttpTransport.php @@ -28,7 +28,7 @@ abstract class AbstractHttpTransport extends AbstractTransport protected $port; protected $client; - public function __construct(HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct(?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) { $this->client = $client; if (null === $client) { @@ -45,7 +45,7 @@ public function __construct(HttpClientInterface $client = null, EventDispatcherI /** * @return $this */ - public function setHost(?string $host) + public function setHost(?string $host): static { $this->host = $host; @@ -55,7 +55,7 @@ public function setHost(?string $host) /** * @return $this */ - public function setPort(?int $port) + public function setPort(?int $port): static { $this->port = $port; @@ -66,7 +66,6 @@ abstract protected function doSendHttp(SentMessage $message): ResponseInterface; protected function doSend(SentMessage $message): void { - $response = null; try { $response = $this->doSendHttp($message); $message->appendDebug($response->getInfo('debug') ?? ''); diff --git a/symfony/mailer/Transport/AbstractTransport.php b/symfony/mailer/Transport/AbstractTransport.php index 85f04a8b8..9a1cd6859 100644 --- a/symfony/mailer/Transport/AbstractTransport.php +++ b/symfony/mailer/Transport/AbstractTransport.php @@ -14,28 +14,30 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\FailedMessageEvent; use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\SentMessageEvent; +use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\BodyRendererInterface; use Symfony\Component\Mime\RawMessage; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface; /** * @author Fabien Potencier */ abstract class AbstractTransport implements TransportInterface { - private $dispatcher; - private $logger; - private $rate = 0; - private $lastSent = 0; + private ?EventDispatcherInterface $dispatcher; + private LoggerInterface $logger; + private float $rate = 0; + private float $lastSent = 0; - public function __construct(EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct(?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) { - $this->dispatcher = class_exists(Event::class) && $dispatcher instanceof SymfonyEventDispatcherInterface ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher; + $this->dispatcher = $dispatcher; $this->logger = $logger ?? new NullLogger(); } @@ -44,7 +46,7 @@ public function __construct(EventDispatcherInterface $dispatcher = null, LoggerI * * @return $this */ - public function setMaxPerSecond(float $rate): self + public function setMaxPerSecond(float $rate): static { if (0 >= $rate) { $rate = 0; @@ -56,24 +58,49 @@ public function setMaxPerSecond(float $rate): self return $this; } - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage { $message = clone $message; $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); - if (null !== $this->dispatcher) { + try { + if (!$this->dispatcher) { + $sentMessage = new SentMessage($message, $envelope); + $this->doSend($sentMessage); + + return $sentMessage; + } + $event = new MessageEvent($message, $envelope, (string) $this); $this->dispatcher->dispatch($event); + if ($event->isRejected()) { + return null; + } + $envelope = $event->getEnvelope(); $message = $event->getMessage(); - } - $message = new SentMessage($message, $envelope); - $this->doSend($message); + if ($message instanceof TemplatedEmail && !$message->isRendered()) { + throw new LogicException(sprintf('You must configure a "%s" when a "%s" instance has a text or HTML template set.', BodyRendererInterface::class, get_debug_type($message))); + } - $this->checkThrottling(); + $sentMessage = new SentMessage($message, $envelope); - return $message; + try { + $this->doSend($sentMessage); + } catch (\Throwable $error) { + $this->dispatcher->dispatch(new FailedMessageEvent($message, $error)); + $this->checkThrottling(); + + throw $error; + } + + $this->dispatcher->dispatch(new SentMessageEvent($sentMessage)); + + return $sentMessage; + } finally { + $this->checkThrottling(); + } } abstract protected function doSend(SentMessage $message): void; @@ -85,9 +112,7 @@ abstract protected function doSend(SentMessage $message): void; */ protected function stringifyAddresses(array $addresses): array { - return array_map(function (Address $a) { - return $a->toString(); - }, $addresses); + return array_map(fn (Address $a) => $a->toString(), $addresses); } protected function getLogger(): LoggerInterface @@ -95,7 +120,7 @@ protected function getLogger(): LoggerInterface return $this->logger; } - private function checkThrottling() + private function checkThrottling(): void { if (0 == $this->rate) { return; @@ -104,7 +129,7 @@ private function checkThrottling() $sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent); if (0 < $sleep) { $this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep)); - usleep($sleep * 1000000); + usleep((int) ($sleep * 1000000)); } $this->lastSent = microtime(true); } diff --git a/symfony/mailer/Transport/AbstractTransportFactory.php b/symfony/mailer/Transport/AbstractTransportFactory.php index e1617d270..96e90f346 100644 --- a/symfony/mailer/Transport/AbstractTransportFactory.php +++ b/symfony/mailer/Transport/AbstractTransportFactory.php @@ -25,7 +25,7 @@ abstract class AbstractTransportFactory implements TransportFactoryInterface protected $client; protected $logger; - public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null) + public function __construct(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $this->client = $client; @@ -41,21 +41,11 @@ abstract protected function getSupportedSchemes(): array; protected function getUser(Dsn $dsn): string { - $user = $dsn->getUser(); - if (null === $user) { - throw new IncompleteDsnException('User is not set.'); - } - - return $user; + return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.'); } protected function getPassword(Dsn $dsn): string { - $password = $dsn->getPassword(); - if (null === $password) { - throw new IncompleteDsnException('Password is not set.'); - } - - return $password; + return $dsn->getPassword() ?? throw new IncompleteDsnException('Password is not set.'); } } diff --git a/symfony/mailer/Transport/Dsn.php b/symfony/mailer/Transport/Dsn.php index 04d3540f7..0cff6aad1 100644 --- a/symfony/mailer/Transport/Dsn.php +++ b/symfony/mailer/Transport/Dsn.php @@ -18,14 +18,14 @@ */ final class Dsn { - private $scheme; - private $host; - private $user; - private $password; - private $port; - private $options; - - public function __construct(string $scheme, string $host, string $user = null, string $password = null, int $port = null, array $options = []) + private string $scheme; + private string $host; + private ?string $user; + private ?string $password; + private ?int $port; + private array $options; + + public function __construct(string $scheme, string $host, ?string $user = null, #[\SensitiveParameter] ?string $password = null, ?int $port = null, array $options = []) { $this->scheme = $scheme; $this->host = $host; @@ -35,26 +35,26 @@ public function __construct(string $scheme, string $host, string $user = null, s $this->options = $options; } - public static function fromString(string $dsn): self + public static function fromString(#[\SensitiveParameter] string $dsn): self { - if (false === $parsedDsn = parse_url($dsn)) { - throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn)); + if (false === $params = parse_url($dsn)) { + throw new InvalidArgumentException('The mailer DSN is invalid.'); } - if (!isset($parsedDsn['scheme'])) { - throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a scheme.', $dsn)); + if (!isset($params['scheme'])) { + throw new InvalidArgumentException('The mailer DSN must contain a scheme.'); } - if (!isset($parsedDsn['host'])) { - throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a host (use "default" by default).', $dsn)); + if (!isset($params['host'])) { + throw new InvalidArgumentException('The mailer DSN must contain a host (use "default" by default).'); } - $user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; - $password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; - $port = $parsedDsn['port'] ?? null; - parse_str($parsedDsn['query'] ?? '', $query); + $user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null; + $password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null; + $port = $params['port'] ?? null; + parse_str($params['query'] ?? '', $query); - return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query); + return new self($params['scheme'], $params['host'], $user, $password, $port, $query); } public function getScheme(): string @@ -77,12 +77,12 @@ public function getPassword(): ?string return $this->password; } - public function getPort(int $default = null): ?int + public function getPort(?int $default = null): ?int { return $this->port ?? $default; } - public function getOption(string $key, $default = null) + public function getOption(string $key, mixed $default = null): mixed { return $this->options[$key] ?? $default; } diff --git a/symfony/mailer/Transport/FailoverTransport.php b/symfony/mailer/Transport/FailoverTransport.php index 49139013f..d6a747730 100644 --- a/symfony/mailer/Transport/FailoverTransport.php +++ b/symfony/mailer/Transport/FailoverTransport.php @@ -18,7 +18,7 @@ */ class FailoverTransport extends RoundRobinTransport { - private $currentTransport; + private ?TransportInterface $currentTransport = null; protected function getNextTransport(): ?TransportInterface { diff --git a/symfony/mailer/Transport/RoundRobinTransport.php b/symfony/mailer/Transport/RoundRobinTransport.php index 761b57f18..ac9709bf7 100644 --- a/symfony/mailer/Transport/RoundRobinTransport.php +++ b/symfony/mailer/Transport/RoundRobinTransport.php @@ -27,10 +27,10 @@ class RoundRobinTransport implements TransportInterface /** * @var \SplObjectStorage */ - private $deadTransports; - private $transports = []; - private $retryPeriod; - private $cursor = -1; + private \SplObjectStorage $deadTransports; + private array $transports = []; + private int $retryPeriod; + private int $cursor = -1; /** * @param TransportInterface[] $transports @@ -46,7 +46,7 @@ public function __construct(array $transports, int $retryPeriod = 60) $this->retryPeriod = $retryPeriod; } - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage { $exception = null; @@ -54,7 +54,7 @@ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessa try { return $transport->send($message, $envelope); } catch (TransportExceptionInterface $e) { - $exception = $exception ?? new TransportException('All transports failed.'); + $exception ??= new TransportException('All transports failed.'); $exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug())); $this->deadTransports[$transport] = microtime(true); } diff --git a/symfony/mailer/Transport/SendmailTransport.php b/symfony/mailer/Transport/SendmailTransport.php index c60f9218c..3add460eb 100644 --- a/symfony/mailer/Transport/SendmailTransport.php +++ b/symfony/mailer/Transport/SendmailTransport.php @@ -33,9 +33,9 @@ */ class SendmailTransport extends AbstractTransport { - private $command = '/usr/sbin/sendmail -bs'; - private $stream; - private $transport; + private string $command = '/usr/sbin/sendmail -bs'; + private ProcessStream $stream; + private ?SmtpTransport $transport = null; /** * Constructor. @@ -49,7 +49,7 @@ class SendmailTransport extends AbstractTransport * * -f flag will be appended automatically if one is not present. */ - public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct(?string $command = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) { parent::__construct($dispatcher, $logger); @@ -64,11 +64,12 @@ public function __construct(string $command = null, EventDispatcherInterface $di $this->stream = new ProcessStream(); if (str_contains($this->command, ' -bs')) { $this->stream->setCommand($this->command); + $this->stream->setInteractive(true); $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger); } } - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage { if ($this->transport) { return $this->transport->send($message, $envelope); diff --git a/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php b/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php index b2ec7b0ee..79cddc469 100644 --- a/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php +++ b/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** @@ -26,8 +27,6 @@ public function getAuthKeyword(): string } /** - * {@inheritdoc} - * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void @@ -41,8 +40,12 @@ public function authenticate(EsmtpTransport $client): void /** * Generates a CRAM-MD5 response from a server challenge. */ - private function getResponse(string $secret, string $challenge): string + private function getResponse(#[\SensitiveParameter] string $secret, string $challenge): string { + if (!$secret) { + throw new InvalidArgumentException('A non-empty secret is required.'); + } + if (\strlen($secret) > 64) { $secret = pack('H32', md5($secret)); } diff --git a/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php b/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php index 1ce321df0..e0b7d577e 100644 --- a/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php +++ b/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php @@ -26,8 +26,6 @@ public function getAuthKeyword(): string } /** - * {@inheritdoc} - * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void diff --git a/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php b/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php index 8d60690f4..6b680b511 100644 --- a/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php +++ b/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php @@ -26,8 +26,6 @@ public function getAuthKeyword(): string } /** - * {@inheritdoc} - * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void diff --git a/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php b/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php index 794177675..c3aaa4909 100644 --- a/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php +++ b/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php @@ -28,8 +28,6 @@ public function getAuthKeyword(): string } /** - * {@inheritdoc} - * * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism */ public function authenticate(EsmtpTransport $client): void diff --git a/symfony/mailer/Transport/Smtp/EsmtpTransport.php b/symfony/mailer/Transport/Smtp/EsmtpTransport.php index 1dcb53f15..f5edfe112 100644 --- a/symfony/mailer/Transport/Smtp/EsmtpTransport.php +++ b/symfony/mailer/Transport/Smtp/EsmtpTransport.php @@ -15,7 +15,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Exception\UnexpectedResponseException; use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; +use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; /** @@ -26,21 +28,26 @@ */ class EsmtpTransport extends SmtpTransport { - private $authenticators = []; - private $username = ''; - private $password = ''; + private array $authenticators = []; + private string $username = ''; + private string $password = ''; + private array $capabilities; - public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, ?AbstractStream $stream = null, ?array $authenticators = null) { - parent::__construct(null, $dispatcher, $logger); - - // order is important here (roughly most secure and popular first) - $this->authenticators = [ - new Auth\CramMd5Authenticator(), - new Auth\LoginAuthenticator(), - new Auth\PlainAuthenticator(), - new Auth\XOAuth2Authenticator(), - ]; + parent::__construct($stream, $dispatcher, $logger); + + if (null === $authenticators) { + // fallback to default authenticators + // order is important here (roughly most secure and popular first) + $authenticators = [ + new Auth\CramMd5Authenticator(), + new Auth\LoginAuthenticator(), + new Auth\PlainAuthenticator(), + new Auth\XOAuth2Authenticator(), + ]; + } + $this->setAuthenticators($authenticators); /** @var SocketStream $stream */ $stream = $this->getStream(); @@ -66,7 +73,7 @@ public function __construct(string $host = 'localhost', int $port = 0, bool $tls /** * @return $this */ - public function setUsername(string $username): self + public function setUsername(string $username): static { $this->username = $username; @@ -81,7 +88,7 @@ public function getUsername(): string /** * @return $this */ - public function setPassword(string $password): self + public function setPassword(#[\SensitiveParameter] string $password): static { $this->password = $password; @@ -93,57 +100,74 @@ public function getPassword(): string return $this->password; } + public function setAuthenticators(array $authenticators): void + { + $this->authenticators = []; + foreach ($authenticators as $authenticator) { + $this->addAuthenticator($authenticator); + } + } + public function addAuthenticator(AuthenticatorInterface $authenticator): void { $this->authenticators[] = $authenticator; } - protected function doHeloCommand(): void + public function executeCommand(string $command, array $codes): string { - if (!$capabilities = $this->callHeloCommand()) { - return; + return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes); + } + + final protected function getCapabilities(): array + { + return $this->capabilities; + } + + private function doEhloCommand(): string + { + try { + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $e) { + try { + return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $ex) { + if (!$ex->getCode()) { + throw $e; + } + + throw $ex; + } } + $this->capabilities = $this->parseCapabilities($response); + /** @var SocketStream $stream */ $stream = $this->getStream(); // WARNING: !$stream->isTLS() is right, 100% sure :) // if you think that the ! should be removed, read the code again // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured - if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) { + if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $this->capabilities)) { $this->executeCommand("STARTTLS\r\n", [220]); if (!$stream->startTLS()) { throw new TransportException('Unable to connect with STARTTLS.'); } - $capabilities = $this->callHeloCommand(); + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + $this->capabilities = $this->parseCapabilities($response); } - if (\array_key_exists('AUTH', $capabilities)) { - $this->handleAuth($capabilities['AUTH']); + if (\array_key_exists('AUTH', $this->capabilities)) { + $this->handleAuth($this->capabilities['AUTH']); } + + return $response; } - private function callHeloCommand(): array + private function parseCapabilities(string $ehloResponse): array { - try { - $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); - } catch (TransportExceptionInterface $e) { - try { - parent::doHeloCommand(); - - return []; - } catch (TransportExceptionInterface $ex) { - if (!$ex->getCode()) { - throw $e; - } - - throw $ex; - } - } - $capabilities = []; - $lines = explode("\r\n", trim($response)); + $lines = explode("\r\n", trim($ehloResponse)); array_shift($lines); foreach ($lines as $line) { if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { @@ -161,6 +185,7 @@ private function handleAuth(array $modes): void return; } + $code = null; $authNames = []; $errors = []; $modes = array_map('strtolower', $modes); @@ -169,15 +194,18 @@ private function handleAuth(array $modes): void continue; } + $code = null; $authNames[] = $authenticator->getAuthKeyword(); try { $authenticator->authenticate($this); return; - } catch (TransportExceptionInterface $e) { + } catch (UnexpectedResponseException $e) { + $code = $e->getCode(); + try { $this->executeCommand("RSET\r\n", [250]); - } catch (TransportExceptionInterface $_) { + } catch (TransportExceptionInterface) { // ignore this exception as it probably means that the server error was final } @@ -187,7 +215,7 @@ private function handleAuth(array $modes): void } if (!$authNames) { - throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes))); + throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504); } $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames)); @@ -195,6 +223,6 @@ private function handleAuth(array $modes): void $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error); } - throw new TransportException($message); + throw new TransportException($message, $code ?: 535); } } diff --git a/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php b/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php index e2a280e49..a15d12245 100644 --- a/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php +++ b/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php @@ -29,17 +29,21 @@ public function create(Dsn $dsn): TransportInterface $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); - if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOLEAN)) { - /** @var SocketStream $stream */ - $stream = $transport->getStream(); - $streamOptions = $stream->getStreamOptions(); + /** @var SocketStream $stream */ + $stream = $transport->getStream(); + $streamOptions = $stream->getStreamOptions(); + if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOL)) { $streamOptions['ssl']['verify_peer'] = false; $streamOptions['ssl']['verify_peer_name'] = false; + } - $stream->setStreamOptions($streamOptions); + if (null !== $peerFingerprint = $dsn->getOption('peer_fingerprint')) { + $streamOptions['ssl']['peer_fingerprint'] = $peerFingerprint; } + $stream->setStreamOptions($streamOptions); + if ($user = $dsn->getUser()) { $transport->setUsername($user); } @@ -52,6 +56,10 @@ public function create(Dsn $dsn): TransportInterface $transport->setLocalDomain($localDomain); } + if (null !== ($maxPerSecond = $dsn->getOption('max_per_second'))) { + $transport->setMaxPerSecond((float) $maxPerSecond); + } + if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) { $transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0)); } diff --git a/symfony/mailer/Transport/Smtp/SmtpTransport.php b/symfony/mailer/Transport/Smtp/SmtpTransport.php index 92af6aaf6..0de38fb2e 100644 --- a/symfony/mailer/Transport/Smtp/SmtpTransport.php +++ b/symfony/mailer/Transport/Smtp/SmtpTransport.php @@ -17,6 +17,7 @@ use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Exception\UnexpectedResponseException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; @@ -31,16 +32,16 @@ */ class SmtpTransport extends AbstractTransport { - private $started = false; - private $restartThreshold = 100; - private $restartThresholdSleep = 0; - private $restartCounter; - private $pingThreshold = 100; - private $lastMessageTime = 0; - private $stream; - private $domain = '[127.0.0.1]'; - - public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + private bool $started = false; + private int $restartThreshold = 100; + private int $restartThresholdSleep = 0; + private int $restartCounter = 0; + private int $pingThreshold = 100; + private float $lastMessageTime = 0; + private AbstractStream $stream; + private string $domain = '[127.0.0.1]'; + + public function __construct(?AbstractStream $stream = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) { parent::__construct($dispatcher, $logger); @@ -62,7 +63,7 @@ public function getStream(): AbstractStream * * @return $this */ - public function setRestartThreshold(int $threshold, int $sleep = 0): self + public function setRestartThreshold(int $threshold, int $sleep = 0): static { $this->restartThreshold = $threshold; $this->restartThresholdSleep = $sleep; @@ -85,7 +86,7 @@ public function setRestartThreshold(int $threshold, int $sleep = 0): self * * @return $this */ - public function setPingThreshold(int $seconds): self + public function setPingThreshold(int $seconds): static { $this->pingThreshold = $seconds; @@ -104,7 +105,7 @@ public function setPingThreshold(int $seconds): self * * @return $this */ - public function setLocalDomain(string $domain): self + public function setLocalDomain(string $domain): static { if ('' !== $domain && '[' !== $domain[0]) { if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { @@ -130,7 +131,7 @@ public function getLocalDomain(): string return $this->domain; } - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage { try { $message = parent::send($message, $envelope); @@ -138,7 +139,7 @@ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessa if ($this->started) { try { $this->executeCommand("RSET\r\n", [250]); - } catch (TransportExceptionInterface $_) { + } catch (TransportExceptionInterface) { // ignore this exception as it probably means that the server error was final } } @@ -151,6 +152,22 @@ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessa return $message; } + protected function parseMessageId(string $mtaResult): string + { + $regexps = [ + '/250 Ok (?P[0-9a-f-]+)\r?$/mis', + '/250 Ok:? queued as (?P[A-Z0-9]+)\r?$/mis', + ]; + $matches = []; + foreach ($regexps as $regexp) { + if (preg_match($regexp, $mtaResult, $matches)) { + return $matches['id']; + } + } + + return ''; + } + public function __toString(): string { if ($this->stream instanceof SocketStream) { @@ -172,8 +189,6 @@ public function __toString(): string * @param int[] $codes * * @throws TransportException when an invalid response if received - * - * @internal */ public function executeCommand(string $command, array $codes): string { @@ -215,9 +230,13 @@ protected function doSend(SentMessage $message): void $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); throw $e; } - $this->executeCommand("\r\n.\r\n", [250]); + $mtaResult = $this->executeCommand("\r\n.\r\n", [250]); $message->appendDebug($this->stream->getDebug()); $this->lastMessageTime = microtime(true); + + if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) { + $message->setMessageId($messageId); + } } catch (TransportExceptionInterface $e) { $e->appendDebug($this->stream->getDebug()); $this->lastMessageTime = 0; @@ -225,6 +244,11 @@ protected function doSend(SentMessage $message): void } } + /** + * @internal since version 6.1, to be made private in 7.0 + * + * @final since version 6.1, to be made private in 7.0 + */ protected function doHeloCommand(): void { $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]); @@ -240,7 +264,7 @@ private function doRcptToCommand(string $address): void $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]); } - private function start(): void + public function start(): void { if ($this->started) { return; @@ -257,7 +281,14 @@ private function start(): void $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__)); } - private function stop(): void + /** + * Manually disconnect from the SMTP server. + * + * In most cases this is not necessary since the disconnect happens automatically on termination. + * In cases of long-running scripts, this might however make sense to avoid keeping an open + * connection to the SMTP server in between sending emails. + */ + public function stop(): void { if (!$this->started) { return; @@ -267,7 +298,7 @@ private function stop(): void try { $this->executeCommand("QUIT\r\n", [221]); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { } finally { $this->stream->terminate(); $this->started = false; @@ -283,7 +314,7 @@ private function ping(): void try { $this->executeCommand("NOOP\r\n", [250]); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { $this->stop(); } } @@ -304,7 +335,7 @@ private function assertResponseCode(string $response, array $codes): void $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code'; $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : ''; - throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0); + throw new UnexpectedResponseException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0); } } @@ -341,14 +372,14 @@ private function checkRestartThreshold(): void $this->restartCounter = 0; } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } + /** + * @return void + */ public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); diff --git a/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php b/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php index 4cfcd8b54..498dc560c 100644 --- a/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php +++ b/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php @@ -24,11 +24,15 @@ */ abstract class AbstractStream { + /** @var resource|null */ protected $stream; + /** @var resource|null */ protected $in; + /** @var resource|null */ protected $out; + protected $err; - private $debug = ''; + private string $debug = ''; public function write(string $bytes, bool $debug = true): void { @@ -65,7 +69,7 @@ abstract public function initialize(): void; public function terminate(): void { - $this->stream = $this->out = $this->in = null; + $this->stream = $this->err = $this->out = $this->in = null; } public function readLine(): string @@ -74,7 +78,7 @@ public function readLine(): string return ''; } - $line = fgets($this->out); + $line = @fgets($this->out); if ('' === $line || false === $line) { $metas = stream_get_meta_data($this->out); if ($metas['timed_out']) { @@ -83,6 +87,9 @@ public function readLine(): string if ($metas['eof']) { throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription())); } + if (false === $line) { + throw new TransportException(sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription()).error_get_last()['message']); + } } $this->debug .= sprintf('< %s', $line); diff --git a/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php b/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php index bc721ad0c..e63514709 100644 --- a/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php +++ b/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php @@ -23,13 +23,19 @@ */ final class ProcessStream extends AbstractStream { - private $command; + private string $command; + private bool $interactive = false; - public function setCommand(string $command) + public function setCommand(string $command): void { $this->command = $command; } + public function setInteractive(bool $interactive): void + { + $this->interactive = $interactive; + } + public function initialize(): void { $descriptorSpec = [ @@ -45,17 +51,27 @@ public function initialize(): void } $this->in = &$pipes[0]; $this->out = &$pipes[1]; + $this->err = &$pipes[2]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); + $out = stream_get_contents($this->out); fclose($this->out); - proc_close($this->stream); + $err = stream_get_contents($this->err); + fclose($this->err); + if (0 !== $exitCode = proc_close($this->stream)) { + $errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err; + } } parent::terminate(); + + if (!$this->interactive && isset($errorMessage)) { + throw new TransportException($errorMessage); + } } protected function getReadConnectionDescription(): string diff --git a/symfony/mailer/Transport/Smtp/Stream/SocketStream.php b/symfony/mailer/Transport/Smtp/Stream/SocketStream.php index 368fbd28c..49e6ea499 100644 --- a/symfony/mailer/Transport/Smtp/Stream/SocketStream.php +++ b/symfony/mailer/Transport/Smtp/Stream/SocketStream.php @@ -23,18 +23,18 @@ */ final class SocketStream extends AbstractStream { - private $url; - private $host = 'localhost'; - private $port = 465; - private $timeout; - private $tls = true; - private $sourceIp; - private $streamContextOptions = []; + private string $url; + private string $host = 'localhost'; + private int $port = 465; + private float $timeout; + private bool $tls = true; + private ?string $sourceIp = null; + private array $streamContextOptions = []; /** * @return $this */ - public function setTimeout(float $timeout): self + public function setTimeout(float $timeout): static { $this->timeout = $timeout; @@ -51,7 +51,7 @@ public function getTimeout(): float * * @return $this */ - public function setHost(string $host): self + public function setHost(string $host): static { $this->host = $host; @@ -66,7 +66,7 @@ public function getHost(): string /** * @return $this */ - public function setPort(int $port): self + public function setPort(int $port): static { $this->port = $port; @@ -83,7 +83,7 @@ public function getPort(): int * * @return $this */ - public function disableTls(): self + public function disableTls(): static { $this->tls = false; @@ -98,7 +98,7 @@ public function isTLS(): bool /** * @return $this */ - public function setStreamOptions(array $options): self + public function setStreamOptions(array $options): static { $this->streamContextOptions = $options; @@ -117,7 +117,7 @@ public function getStreamOptions(): array * * @return $this */ - public function setSourceIp(string $ip): self + public function setSourceIp(string $ip): static { $this->sourceIp = $ip; @@ -146,7 +146,7 @@ public function initialize(): void $options = array_merge($options, $this->streamContextOptions); } // do it unconditionally as it will be used by STARTTLS as well if supported - $options['ssl']['crypto_method'] = $options['ssl']['crypto_method'] ?? \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + $options['ssl']['crypto_method'] ??= \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; $streamContext = stream_context_create($options); $timeout = $this->getTimeout(); @@ -160,7 +160,7 @@ public function initialize(): void } stream_set_blocking($this->stream, true); - stream_set_timeout($this->stream, $timeout); + stream_set_timeout($this->stream, (int) $timeout, (int) (($timeout - (int) $timeout) * 1000000)); $this->in = &$this->stream; $this->out = &$this->stream; } diff --git a/symfony/mailer/Transport/TransportInterface.php b/symfony/mailer/Transport/TransportInterface.php index ed562cfef..01570cab4 100644 --- a/symfony/mailer/Transport/TransportInterface.php +++ b/symfony/mailer/Transport/TransportInterface.php @@ -24,12 +24,10 @@ * * @author Fabien Potencier */ -interface TransportInterface +interface TransportInterface extends \Stringable { /** * @throws TransportExceptionInterface */ - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage; - - public function __toString(): string; + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage; } diff --git a/symfony/mailer/Transport/Transports.php b/symfony/mailer/Transport/Transports.php index 702fc5c78..f02b9bc9b 100644 --- a/symfony/mailer/Transport/Transports.php +++ b/symfony/mailer/Transport/Transports.php @@ -23,19 +23,19 @@ */ final class Transports implements TransportInterface { - private $transports; - private $default; + /** + * @var array + */ + private array $transports = []; + private TransportInterface $default; /** - * @param TransportInterface[] $transports + * @param iterable $transports */ public function __construct(iterable $transports) { - $this->transports = []; foreach ($transports as $name => $transport) { - if (null === $this->default) { - $this->default = $transport; - } + $this->default ??= $transport; $this->transports[$name] = $transport; } @@ -44,10 +44,10 @@ public function __construct(iterable $transports) } } - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage { /** @var Message $message */ - if (RawMessage::class === \get_class($message) || !$message->getHeaders()->has('X-Transport')) { + if (RawMessage::class === $message::class || !$message->getHeaders()->has('X-Transport')) { return $this->default->send($message, $envelope); } diff --git a/symfony/mime/Address.php b/symfony/mime/Address.php index ae83efd73..ce57f77ee 100644 --- a/symfony/mime/Address.php +++ b/symfony/mime/Address.php @@ -33,21 +33,19 @@ final class Address */ private const FROM_STRING_PATTERN = '~(?[^<]*)<(?.*)>[^>]*~'; - private static $validator; - private static $encoder; + private static EmailValidator $validator; + private static IdnAddressEncoder $encoder; - private $address; - private $name; + private string $address; + private string $name; public function __construct(string $address, string $name = '') { if (!class_exists(EmailValidator::class)) { - throw new LogicException(sprintf('The "%s" class cannot be used as it needs "%s"; try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class)); + throw new LogicException(sprintf('The "%s" class cannot be used as it needs "%s". Try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class)); } - if (null === self::$validator) { - self::$validator = new EmailValidator(); - } + self::$validator ??= new EmailValidator(); $this->address = trim($address); $this->name = trim(str_replace(["\n", "\r"], '', $name)); @@ -69,9 +67,7 @@ public function getName(): string public function getEncodedAddress(): string { - if (null === self::$encoder) { - self::$encoder = new IdnAddressEncoder(); - } + self::$encoder ??= new IdnAddressEncoder(); return self::$encoder->encodeString($this->address); } @@ -90,20 +86,13 @@ public function getEncodedName(): string return sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName())); } - /** - * @param Address|string $address - */ - public static function create($address): self + public static function create(self|string $address): self { if ($address instanceof self) { return $address; } - if (!\is_string($address)) { - throw new InvalidArgumentException(sprintf('An address can be an instance of Address or a string ("%s" given).', get_debug_type($address))); - } - - if (false === strpos($address, '<')) { + if (!str_contains($address, '<')) { return new self($address); } @@ -128,22 +117,4 @@ public static function createArray(array $addresses): array return $addrs; } - - /** - * @deprecated since Symfony 5.2, use "create()" instead. - */ - public static function fromString(string $string): self - { - trigger_deprecation('symfony/mime', '5.2', '"%s()" is deprecated, use "%s::create()" instead.', __METHOD__, __CLASS__); - - if (!str_contains($string, '<')) { - return new self($string, ''); - } - - if (!preg_match(self::FROM_STRING_PATTERN, $string, $matches)) { - throw new InvalidArgumentException(sprintf('Could not parse "%s" to a "%s" instance.', $string, self::class)); - } - - return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"')); - } } diff --git a/symfony/mime/CharacterStream.php b/symfony/mime/CharacterStream.php index 238debde1..21d7bc5f0 100644 --- a/symfony/mime/CharacterStream.php +++ b/symfony/mime/CharacterStream.php @@ -55,12 +55,12 @@ final class CharacterStream "\xf8" => 5, "\xf9" => 5, "\xfa" => 5, "\xfb" => 5, "\xfc" => 6, "\xfd" => 6, "\xfe" => 0, "\xff" => 0, ]; - private $data = ''; - private $dataSize = 0; - private $map = []; - private $charCount = 0; - private $currentPos = 0; - private $fixedWidth = 0; + private string $data = ''; + private int $dataSize = 0; + private array $map = []; + private int $charCount = 0; + private int $currentPos = 0; + private int $fixedWidth = 0; /** * @param resource|string $input @@ -72,29 +72,22 @@ public function __construct($input, ?string $charset = 'utf-8') $this->fixedWidth = 0; $this->map = ['p' => [], 'i' => []]; } else { - switch ($charset) { + $this->fixedWidth = match ($charset) { // 16 bits - case 'ucs2': - case 'ucs-2': - case 'utf16': - case 'utf-16': - $this->fixedWidth = 2; - break; - - // 32 bits - case 'ucs4': - case 'ucs-4': - case 'utf32': - case 'utf-32': - $this->fixedWidth = 4; - break; - - // 7-8 bit charsets: (us-)?ascii, (iso|iec)-?8859-?[0-9]+, windows-?125[0-9], cp-?[0-9]+, ansi, macintosh, + 'ucs2', + 'ucs-2', + 'utf16', + 'utf-16' => 2, + // 32 bits + 'ucs4', + 'ucs-4', + 'utf32', + 'utf-32' => 4, + // 7-8 bit charsets: (us-)?ascii, (iso|iec)-?8859-?[0-9]+, windows-?125[0-9], cp-?[0-9]+, ansi, macintosh, // koi-?7, koi-?8-?.+, mik, (cork|t1), v?iscii - // and fallback - default: - $this->fixedWidth = 1; - } + // and fallback + default => 1, + }; } if (\is_resource($input)) { $blocks = 16372; diff --git a/symfony/mime/Crypto/DkimOptions.php b/symfony/mime/Crypto/DkimOptions.php index 171bb2583..cee4e7cb8 100644 --- a/symfony/mime/Crypto/DkimOptions.php +++ b/symfony/mime/Crypto/DkimOptions.php @@ -18,7 +18,7 @@ */ final class DkimOptions { - private $options = []; + private array $options = []; public function toArray(): array { @@ -28,7 +28,7 @@ public function toArray(): array /** * @return $this */ - public function algorithm(string $algo): self + public function algorithm(string $algo): static { $this->options['algorithm'] = $algo; @@ -38,7 +38,7 @@ public function algorithm(string $algo): self /** * @return $this */ - public function signatureExpirationDelay(int $show): self + public function signatureExpirationDelay(int $show): static { $this->options['signature_expiration_delay'] = $show; @@ -48,7 +48,7 @@ public function signatureExpirationDelay(int $show): self /** * @return $this */ - public function bodyMaxLength(int $max): self + public function bodyMaxLength(int $max): static { $this->options['body_max_length'] = $max; @@ -58,7 +58,7 @@ public function bodyMaxLength(int $max): self /** * @return $this */ - public function bodyShowLength(bool $show): self + public function bodyShowLength(bool $show): static { $this->options['body_show_length'] = $show; @@ -68,7 +68,7 @@ public function bodyShowLength(bool $show): self /** * @return $this */ - public function headerCanon(string $canon): self + public function headerCanon(string $canon): static { $this->options['header_canon'] = $canon; @@ -78,7 +78,7 @@ public function headerCanon(string $canon): self /** * @return $this */ - public function bodyCanon(string $canon): self + public function bodyCanon(string $canon): static { $this->options['body_canon'] = $canon; @@ -88,7 +88,7 @@ public function bodyCanon(string $canon): self /** * @return $this */ - public function headersToIgnore(array $headers): self + public function headersToIgnore(array $headers): static { $this->options['headers_to_ignore'] = $headers; diff --git a/symfony/mime/Crypto/DkimSigner.php b/symfony/mime/Crypto/DkimSigner.php index f0f7091ed..1d2005e5c 100644 --- a/symfony/mime/Crypto/DkimSigner.php +++ b/symfony/mime/Crypto/DkimSigner.php @@ -30,10 +30,10 @@ final class DkimSigner public const ALGO_SHA256 = 'rsa-sha256'; public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463 - private $key; - private $domainName; - private $selector; - private $defaultOptions; + private \OpenSSLAsymmetricKey $key; + private string $domainName; + private string $selector; + private array $defaultOptions; /** * @param string $pk The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format) @@ -44,10 +44,7 @@ public function __construct(string $pk, string $domainName, string $selector, ar if (!\extension_loaded('openssl')) { throw new \LogicException('PHP extension "openssl" is required to use DKIM.'); } - if (!$this->key = openssl_pkey_get_private($pk, $passphrase)) { - throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string()); - } - + $this->key = openssl_pkey_get_private($pk, $passphrase) ?: throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string()); $this->domainName = $domainName; $this->selector = $selector; $this->defaultOptions = $defaultOptions + [ diff --git a/symfony/mime/Crypto/SMimeEncrypter.php b/symfony/mime/Crypto/SMimeEncrypter.php index 9081860d8..c7c05452c 100644 --- a/symfony/mime/Crypto/SMimeEncrypter.php +++ b/symfony/mime/Crypto/SMimeEncrypter.php @@ -19,21 +19,21 @@ */ final class SMimeEncrypter extends SMime { - private $certs; - private $cipher; + private string|array $certs; + private int $cipher; /** * @param string|string[] $certificate The path (or array of paths) of the file(s) containing the X.509 certificate(s) * @param int|null $cipher A set of algorithms used to encrypt the message. Must be one of these PHP constants: https://www.php.net/manual/en/openssl.ciphers.php */ - public function __construct($certificate, int $cipher = null) + public function __construct(string|array $certificate, ?int $cipher = null) { if (!\extension_loaded('openssl')) { throw new \LogicException('PHP extension "openssl" is required to use SMime.'); } if (\is_array($certificate)) { - $this->certs = array_map([$this, 'normalizeFilePath'], $certificate); + $this->certs = array_map($this->normalizeFilePath(...), $certificate); } else { $this->certs = $this->normalizeFilePath($certificate); } diff --git a/symfony/mime/Crypto/SMimeSigner.php b/symfony/mime/Crypto/SMimeSigner.php index 5b94a454e..eaa423d8c 100644 --- a/symfony/mime/Crypto/SMimeSigner.php +++ b/symfony/mime/Crypto/SMimeSigner.php @@ -19,10 +19,10 @@ */ final class SMimeSigner extends SMime { - private $signCertificate; - private $signPrivateKey; - private $signOptions; - private $extraCerts; + private string $signCertificate; + private string|array $signPrivateKey; + private int $signOptions; + private ?string $extraCerts; /** * @param string $certificate The path of the file containing the signing certificate (in PEM format) @@ -31,7 +31,7 @@ final class SMimeSigner extends SMime * @param string|null $extraCerts The path of the file containing intermediate certificates (in PEM format) needed by the signing certificate * @param int|null $signOptions Bitwise operator options for openssl_pkcs7_sign() (@see https://secure.php.net/manual/en/openssl.pkcs7.flags.php) */ - public function __construct(string $certificate, string $privateKey, string $privateKeyPassphrase = null, string $extraCerts = null, int $signOptions = null) + public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, ?int $signOptions = null) { if (!\extension_loaded('openssl')) { throw new \LogicException('PHP extension "openssl" is required to use SMime.'); diff --git a/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php b/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php index 00eef94ee..70fa78633 100644 --- a/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php +++ b/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php @@ -22,27 +22,14 @@ */ class AddMimeTypeGuesserPass implements CompilerPassInterface { - private $mimeTypesService; - private $mimeTypeGuesserTag; - - public function __construct(string $mimeTypesService = 'mime_types', string $mimeTypeGuesserTag = 'mime.mime_type_guesser') - { - if (0 < \func_num_args()) { - trigger_deprecation('symfony/mime', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); - } - - $this->mimeTypesService = $mimeTypesService; - $this->mimeTypeGuesserTag = $mimeTypeGuesserTag; - } - /** - * {@inheritdoc} + * @return void */ public function process(ContainerBuilder $container) { - if ($container->has($this->mimeTypesService)) { - $definition = $container->findDefinition($this->mimeTypesService); - foreach ($container->findTaggedServiceIds($this->mimeTypeGuesserTag, true) as $id => $attributes) { + if ($container->has('mime_types')) { + $definition = $container->findDefinition('mime_types'); + foreach ($container->findTaggedServiceIds('mime.mime_type_guesser', true) as $id => $attributes) { $definition->addMethodCall('registerGuesser', [new Reference($id)]); } } diff --git a/symfony/mime/DraftEmail.php b/symfony/mime/DraftEmail.php new file mode 100644 index 000000000..82ce76369 --- /dev/null +++ b/symfony/mime/DraftEmail.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Part\AbstractPart; + +/** + * @author Kevin Bond + */ +class DraftEmail extends Email +{ + public function __construct(?Headers $headers = null, ?AbstractPart $body = null) + { + parent::__construct($headers, $body); + + $this->getHeaders()->addTextHeader('X-Unsent', '1'); + } + + /** + * Override default behavior as draft emails do not require From/Sender/Date/Message-ID headers. + * These are added by the client that actually sends the email. + */ + public function getPreparedHeaders(): Headers + { + $headers = clone $this->getHeaders(); + + if (!$headers->has('MIME-Version')) { + $headers->addTextHeader('MIME-Version', '1.0'); + } + + $headers->remove('Bcc'); + + return $headers; + } +} diff --git a/symfony/mime/Email.php b/symfony/mime/Email.php index bd0a476c4..346618cf2 100644 --- a/symfony/mime/Email.php +++ b/symfony/mime/Email.php @@ -14,6 +14,7 @@ use Symfony\Component\Mime\Exception\LogicException; use Symfony\Component\Mime\Part\AbstractPart; use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\File; use Symfony\Component\Mime\Part\Multipart\AlternativePart; use Symfony\Component\Mime\Part\Multipart\MixedPart; use Symfony\Component\Mime\Part\Multipart\RelatedPart; @@ -38,20 +39,26 @@ class Email extends Message self::PRIORITY_LOWEST => 'Lowest', ]; + /** + * @var resource|string|null + */ private $text; - private $textCharset; - private $html; - private $htmlCharset; - private $attachments = []; + + private ?string $textCharset = null; + /** - * @var AbstractPart|null + * @var resource|string|null */ - private $cachedBody; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries. + private $html; + + private ?string $htmlCharset = null; + private array $attachments = []; + private ?AbstractPart $cachedBody = null; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries. /** * @return $this */ - public function subject(string $subject) + public function subject(string $subject): static { return $this->setHeaderBody('Text', 'Subject', $subject); } @@ -64,7 +71,7 @@ public function getSubject(): ?string /** * @return $this */ - public function date(\DateTimeInterface $dateTime) + public function date(\DateTimeInterface $dateTime): static { return $this->setHeaderBody('Date', 'Date', $dateTime); } @@ -75,11 +82,9 @@ public function getDate(): ?\DateTimeImmutable } /** - * @param Address|string $address - * * @return $this */ - public function returnPath($address) + public function returnPath(Address|string $address): static { return $this->setHeaderBody('Path', 'Return-Path', Address::create($address)); } @@ -90,11 +95,9 @@ public function getReturnPath(): ?Address } /** - * @param Address|string $address - * * @return $this */ - public function sender($address) + public function sender(Address|string $address): static { return $this->setHeaderBody('Mailbox', 'Sender', Address::create($address)); } @@ -105,22 +108,22 @@ public function getSender(): ?Address } /** - * @param Address|string ...$addresses - * * @return $this */ - public function addFrom(...$addresses) + public function addFrom(Address|string ...$addresses): static { return $this->addListAddressHeaderBody('From', $addresses); } /** - * @param Address|string ...$addresses - * * @return $this */ - public function from(...$addresses) + public function from(Address|string ...$addresses): static { + if (!$addresses) { + throw new LogicException('"from()" must be called with at least one address.'); + } + return $this->setListAddressHeaderBody('From', $addresses); } @@ -133,21 +136,17 @@ public function getFrom(): array } /** - * @param Address|string ...$addresses - * * @return $this */ - public function addReplyTo(...$addresses) + public function addReplyTo(Address|string ...$addresses): static { return $this->addListAddressHeaderBody('Reply-To', $addresses); } /** - * @param Address|string ...$addresses - * * @return $this */ - public function replyTo(...$addresses) + public function replyTo(Address|string ...$addresses): static { return $this->setListAddressHeaderBody('Reply-To', $addresses); } @@ -161,21 +160,17 @@ public function getReplyTo(): array } /** - * @param Address|string ...$addresses - * * @return $this */ - public function addTo(...$addresses) + public function addTo(Address|string ...$addresses): static { return $this->addListAddressHeaderBody('To', $addresses); } /** - * @param Address|string ...$addresses - * * @return $this */ - public function to(...$addresses) + public function to(Address|string ...$addresses): static { return $this->setListAddressHeaderBody('To', $addresses); } @@ -189,21 +184,17 @@ public function getTo(): array } /** - * @param Address|string ...$addresses - * * @return $this */ - public function addCc(...$addresses) + public function addCc(Address|string ...$addresses): static { return $this->addListAddressHeaderBody('Cc', $addresses); } /** - * @param Address|string ...$addresses - * * @return $this */ - public function cc(...$addresses) + public function cc(Address|string ...$addresses): static { return $this->setListAddressHeaderBody('Cc', $addresses); } @@ -217,21 +208,17 @@ public function getCc(): array } /** - * @param Address|string ...$addresses - * * @return $this */ - public function addBcc(...$addresses) + public function addBcc(Address|string ...$addresses): static { return $this->addListAddressHeaderBody('Bcc', $addresses); } /** - * @param Address|string ...$addresses - * * @return $this */ - public function bcc(...$addresses) + public function bcc(Address|string ...$addresses): static { return $this->setListAddressHeaderBody('Bcc', $addresses); } @@ -251,7 +238,7 @@ public function getBcc(): array * * @return $this */ - public function priority(int $priority) + public function priority(int $priority): static { if ($priority > 5) { $priority = 5; @@ -280,7 +267,7 @@ public function getPriority(): int * * @return $this */ - public function text($body, string $charset = 'utf-8') + public function text($body, string $charset = 'utf-8'): static { if (null !== $body && !\is_string($body) && !\is_resource($body)) { throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); @@ -311,7 +298,7 @@ public function getTextCharset(): ?string * * @return $this */ - public function html($body, string $charset = 'utf-8') + public function html($body, string $charset = 'utf-8'): static { if (null !== $body && !\is_string($body) && !\is_resource($body)) { throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); @@ -342,27 +329,17 @@ public function getHtmlCharset(): ?string * * @return $this */ - public function attach($body, string $name = null, string $contentType = null) + public function attach($body, ?string $name = null, ?string $contentType = null): static { - if (!\is_string($body) && !\is_resource($body)) { - throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body))); - } - - $this->cachedBody = null; - $this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => false]; - - return $this; + return $this->addPart(new DataPart($body, $name, $contentType)); } /** * @return $this */ - public function attachFromPath(string $path, string $name = null, string $contentType = null) + public function attachFromPath(string $path, ?string $name = null, ?string $contentType = null): static { - $this->cachedBody = null; - $this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => false]; - - return $this; + return $this->addPart(new DataPart(new File($path), $name, $contentType)); } /** @@ -370,51 +347,48 @@ public function attachFromPath(string $path, string $name = null, string $conten * * @return $this */ - public function embed($body, string $name = null, string $contentType = null) + public function embed($body, ?string $name = null, ?string $contentType = null): static { - if (!\is_string($body) && !\is_resource($body)) { - throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body))); - } - - $this->cachedBody = null; - $this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => true]; + return $this->addPart((new DataPart($body, $name, $contentType))->asInline()); + } - return $this; + /** + * @return $this + */ + public function embedFromPath(string $path, ?string $name = null, ?string $contentType = null): static + { + return $this->addPart((new DataPart(new File($path), $name, $contentType))->asInline()); } /** * @return $this + * + * @deprecated since Symfony 6.2, use addPart() instead */ - public function embedFromPath(string $path, string $name = null, string $contentType = null) + public function attachPart(DataPart $part): static { - $this->cachedBody = null; - $this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => true]; + @trigger_deprecation('symfony/mime', '6.2', 'The "%s()" method is deprecated, use "addPart()" instead.', __METHOD__); - return $this; + return $this->addPart($part); } /** * @return $this */ - public function attachPart(DataPart $part) + public function addPart(DataPart $part): static { $this->cachedBody = null; - $this->attachments[] = ['part' => $part]; + $this->attachments[] = $part; return $this; } /** - * @return array|DataPart[] + * @return DataPart[] */ public function getAttachments(): array { - $parts = []; - foreach ($this->attachments as $attachment) { - $parts[] = $this->createDataPart($attachment); - } - - return $parts; + return $this->attachments; } public function getBody(): AbstractPart @@ -426,15 +400,27 @@ public function getBody(): AbstractPart return $this->generateBody(); } + /** + * @return void + */ public function ensureValidity() { - if (null === $this->text && null === $this->html && !$this->attachments) { - throw new LogicException('A message must have a text or an HTML part or attachments.'); + $this->ensureBodyValid(); + + if ('1' === $this->getHeaders()->getHeaderBody('X-Unsent')) { + throw new LogicException('Cannot send messages marked as "draft".'); } parent::ensureValidity(); } + private function ensureBodyValid(): void + { + if (null === $this->text && null === $this->html && !$this->attachments) { + throw new LogicException('A message must have a text or an HTML part or attachments.'); + } + } + /** * Generates an AbstractPart based on the raw body of a message. * @@ -461,7 +447,7 @@ private function generateBody(): AbstractPart return $this->cachedBody; } - $this->ensureValidity(); + $this->ensureBodyValid(); [$htmlPart, $otherParts, $relatedParts] = $this->prepareParts(); @@ -497,43 +483,39 @@ private function prepareParts(): ?array if (null !== $html) { $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); $html = $htmlPart->getBody(); - preg_match_all('(]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+)))i', $html, $names); - $names = array_filter(array_unique(array_merge($names[2], $names[3]))); - } - // usage of reflection is a temporary workaround for missing getters that will be added in 6.2 - $nameRef = new \ReflectionProperty(TextPart::class, 'name'); - $nameRef->setAccessible(true); - $otherParts = $relatedParts = []; - foreach ($this->attachments as $attachment) { - $part = $this->createDataPart($attachment); - if (isset($attachment['part'])) { - $attachment['name'] = $nameRef->getValue($part); + $regexes = [ + ']*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))', + '<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))', + ]; + $tmpMatches = []; + foreach ($regexes as $regex) { + preg_match_all('/'.$regex.'/i', $html, $tmpMatches); + $names = array_merge($names, $tmpMatches[2], $tmpMatches[3]); } + $names = array_filter(array_unique($names)); + } - $related = false; + $otherParts = $relatedParts = []; + foreach ($this->attachments as $part) { foreach ($names as $name) { - if ($name !== $attachment['name']) { + if ($name !== $part->getName() && (!$part->hasContentId() || $name !== $part->getContentId())) { continue; } if (isset($relatedParts[$name])) { continue 2; } - $part->setDisposition('inline'); - $html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html, $count); - if ($count) { - $related = true; + + if ($name !== $part->getContentId()) { + $html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html, $count); } - $part->setName($part->getContentId()); + $relatedParts[$name] = $part; + $part->setName($part->getContentId())->asInline(); - break; + continue 2; } - if ($related) { - $relatedParts[$attachment['name']] = $part; - } else { - $otherParts[] = $part; - } + $otherParts[] = $part; } if (null !== $htmlPart) { $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); @@ -542,35 +524,20 @@ private function prepareParts(): ?array return [$htmlPart, $otherParts, array_values($relatedParts)]; } - private function createDataPart(array $attachment): DataPart - { - if (isset($attachment['part'])) { - return $attachment['part']; - } - - if (isset($attachment['body'])) { - $part = new DataPart($attachment['body'], $attachment['name'] ?? null, $attachment['content-type'] ?? null); - } else { - $part = DataPart::fromPath($attachment['path'] ?? '', $attachment['name'] ?? null, $attachment['content-type'] ?? null); - } - if ($attachment['inline']) { - $part->asInline(); - } - - return $part; - } - /** * @return $this */ - private function setHeaderBody(string $type, string $name, $body): object + private function setHeaderBody(string $type, string $name, $body): static { $this->getHeaders()->setHeaderBody($type, $name, $body); return $this; } - private function addListAddressHeaderBody(string $name, array $addresses) + /** + * @return $this + */ + private function addListAddressHeaderBody(string $name, array $addresses): static { if (!$header = $this->getHeaders()->get($name)) { return $this->setListAddressHeaderBody($name, $addresses); @@ -583,7 +550,7 @@ private function addListAddressHeaderBody(string $name, array $addresses) /** * @return $this */ - private function setListAddressHeaderBody(string $name, array $addresses) + private function setListAddressHeaderBody(string $name, array $addresses): static { $addresses = Address::createArray($addresses); $headers = $this->getHeaders(); @@ -609,12 +576,6 @@ public function __serialize(): array $this->html = (new TextPart($this->html))->getBody(); } - foreach ($this->attachments as $i => $attachment) { - if (isset($attachment['body']) && \is_resource($attachment['body'])) { - $this->attachments[$i]['body'] = (new TextPart($attachment['body']))->getBody(); - } - } - return [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, parent::__serialize()]; } diff --git a/symfony/mime/Encoder/QpContentEncoder.php b/symfony/mime/Encoder/QpContentEncoder.php index 4703cc2e6..6f420fff3 100644 --- a/symfony/mime/Encoder/QpContentEncoder.php +++ b/symfony/mime/Encoder/QpContentEncoder.php @@ -46,15 +46,10 @@ private function standardize(string $string): string // transform =0D=0A to CRLF $string = str_replace(["\t=0D=0A", ' =0D=0A', '=0D=0A'], ["=09\r\n", "=20\r\n", "\r\n"], $string); - switch (\ord(substr($string, -1))) { - case 0x09: - $string = substr_replace($string, '=09', -1); - break; - case 0x20: - $string = substr_replace($string, '=20', -1); - break; - } - - return $string; + return match (\ord(substr($string, -1))) { + 0x09 => substr_replace($string, '=09', -1), + 0x20 => substr_replace($string, '=20', -1), + default => $string, + }; } } diff --git a/symfony/mime/Encoder/QpEncoder.php b/symfony/mime/Encoder/QpEncoder.php index b861af232..160dde329 100644 --- a/symfony/mime/Encoder/QpEncoder.php +++ b/symfony/mime/Encoder/QpEncoder.php @@ -76,7 +76,7 @@ class QpEncoder implements EncoderInterface 255 => '=FF', ]; - private static $safeMapShare = []; + private static array $safeMapShare = []; /** * A map of non-encoded ascii characters. @@ -85,7 +85,7 @@ class QpEncoder implements EncoderInterface * * @internal */ - protected $safeMap = []; + protected array $safeMap = []; public function __construct() { @@ -106,8 +106,6 @@ protected function initSafeMap(): void } /** - * {@inheritdoc} - * * Takes an unencoded string and produces a QP encoded string from it. * * QP encoded strings have a maximum line length of 76 characters. @@ -184,12 +182,11 @@ private function encodeByteSequence(array $bytes, int &$size): string private function standardize(string $string): string { $string = str_replace(["\t=0D=0A", ' =0D=0A', '=0D=0A'], ["=09\r\n", "=20\r\n", "\r\n"], $string); - switch ($end = \ord(substr($string, -1))) { - case 0x09: - case 0x20: - $string = substr_replace($string, self::QP_MAP[$end], -1); - } - return $string; + return match ($end = \ord(substr($string, -1))) { + 0x09, + 0x20 => substr_replace($string, self::QP_MAP[$end], -1), + default => $string, + }; } } diff --git a/symfony/mime/FileBinaryMimeTypeGuesser.php b/symfony/mime/FileBinaryMimeTypeGuesser.php index 3acf4d455..83e2950ce 100644 --- a/symfony/mime/FileBinaryMimeTypeGuesser.php +++ b/symfony/mime/FileBinaryMimeTypeGuesser.php @@ -21,7 +21,7 @@ */ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface { - private $cmd; + private string $cmd; /** * The $cmd pattern must contain a "%s" string that will be replaced @@ -36,9 +36,6 @@ public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null') $this->cmd = $cmd; } - /** - * {@inheritdoc} - */ public function isGuesserSupported(): bool { static $supported = null; @@ -58,9 +55,6 @@ public function isGuesserSupported(): bool return $supported = 0 === $exitStatus && '' !== $binPath; } - /** - * {@inheritdoc} - */ public function guessMimeType(string $path): ?string { if (!is_file($path) || !is_readable($path)) { diff --git a/symfony/mime/FileinfoMimeTypeGuesser.php b/symfony/mime/FileinfoMimeTypeGuesser.php index c6c7559af..776124f8c 100644 --- a/symfony/mime/FileinfoMimeTypeGuesser.php +++ b/symfony/mime/FileinfoMimeTypeGuesser.php @@ -21,29 +21,23 @@ */ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface { - private $magicFile; + private ?string $magicFile; /** - * @param string $magicFile A magic file to use with the finfo instance + * @param string|null $magicFile A magic file to use with the finfo instance * * @see http://www.php.net/manual/en/function.finfo-open.php */ - public function __construct(string $magicFile = null) + public function __construct(?string $magicFile = null) { $this->magicFile = $magicFile; } - /** - * {@inheritdoc} - */ public function isGuesserSupported(): bool { return \function_exists('finfo_open'); } - /** - * {@inheritdoc} - */ public function guessMimeType(string $path): ?string { if (!is_file($path) || !is_readable($path)) { diff --git a/symfony/mime/Header/AbstractHeader.php b/symfony/mime/Header/AbstractHeader.php index 5de906687..9994ec611 100644 --- a/symfony/mime/Header/AbstractHeader.php +++ b/symfony/mime/Header/AbstractHeader.php @@ -22,18 +22,21 @@ abstract class AbstractHeader implements HeaderInterface { public const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)'; - private static $encoder; + private static QpMimeHeaderEncoder $encoder; - private $name; - private $lineLength = 76; - private $lang; - private $charset = 'utf-8'; + private string $name; + private int $lineLength = 76; + private ?string $lang = null; + private string $charset = 'utf-8'; public function __construct(string $name) { $this->name = $name; } + /** + * @return void + */ public function setCharset(string $charset) { $this->charset = $charset; @@ -48,6 +51,8 @@ public function getCharset(): ?string * Set the language used in this Header. * * For example, for US English, 'en-us'. + * + * @return void */ public function setLanguage(string $lang) { @@ -64,6 +69,9 @@ public function getName(): string return $this->name; } + /** + * @return void + */ public function setMaxLineLength(int $lineLength) { $this->lineLength = $lineLength; @@ -188,9 +196,7 @@ protected function getEncodableWordTokens(string $string): array */ protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string { - if (null === self::$encoder) { - self::$encoder = new QpMimeHeaderEncoder(); - } + self::$encoder ??= new QpMimeHeaderEncoder(); // Adjust $firstLineOffset to account for space needed for syntax $charsetDecl = $this->charset; @@ -231,11 +237,9 @@ protected function generateTokenLines(string $token): array /** * Generate a list of all tokens in the final header. */ - protected function toTokens(string $string = null): array + protected function toTokens(?string $string = null): array { - if (null === $string) { - $string = $this->getBodyAsString(); - } + $string ??= $this->getBodyAsString(); $tokens = []; // Generate atoms; split at all invisible boundaries followed by WSP @@ -265,8 +269,8 @@ private function tokensToString(array $tokens): string // Build all tokens back into compliant header foreach ($tokens as $i => $token) { // Line longer than specified maximum or token was just a new line - if (("\r\n" === $token) || - ($i > 0 && \strlen($currentLine.$token) > $this->lineLength) + if (("\r\n" === $token) + || ($i > 0 && \strlen($currentLine.$token) > $this->lineLength) && '' !== $currentLine) { $headerLines[] = ''; $currentLine = &$headerLines[$lineCount++]; diff --git a/symfony/mime/Header/DateHeader.php b/symfony/mime/Header/DateHeader.php index a7385d4ce..2b361802f 100644 --- a/symfony/mime/Header/DateHeader.php +++ b/symfony/mime/Header/DateHeader.php @@ -18,7 +18,7 @@ */ final class DateHeader extends AbstractHeader { - private $dateTime; + private \DateTimeImmutable $dateTime; public function __construct(string $name, \DateTimeInterface $date) { @@ -30,7 +30,7 @@ public function __construct(string $name, \DateTimeInterface $date) /** * @param \DateTimeInterface $body */ - public function setBody($body) + public function setBody(mixed $body): void { $this->setDateTime($body); } @@ -50,17 +50,13 @@ public function getDateTime(): \DateTimeImmutable * * If a DateTime instance is provided, it is converted to DateTimeImmutable. */ - public function setDateTime(\DateTimeInterface $dateTime) + public function setDateTime(\DateTimeInterface $dateTime): void { - if ($dateTime instanceof \DateTime) { - $immutable = new \DateTimeImmutable('@'.$dateTime->getTimestamp()); - $dateTime = $immutable->setTimezone($dateTime->getTimezone()); - } - $this->dateTime = $dateTime; + $this->dateTime = \DateTimeImmutable::createFromInterface($dateTime); } public function getBodyAsString(): string { - return $this->dateTime->format(\DateTime::RFC2822); + return $this->dateTime->format(\DateTimeInterface::RFC2822); } } diff --git a/symfony/mime/Header/HeaderInterface.php b/symfony/mime/Header/HeaderInterface.php index 4546947c7..5bc4162c3 100644 --- a/symfony/mime/Header/HeaderInterface.php +++ b/symfony/mime/Header/HeaderInterface.php @@ -23,29 +23,36 @@ interface HeaderInterface * * The type depends on the Header concrete class. * - * @param mixed $body + * @return void */ - public function setBody($body); + public function setBody(mixed $body); /** * Gets the body. * * The return type depends on the Header concrete class. - * - * @return mixed */ - public function getBody(); + public function getBody(): mixed; + /** + * @return void + */ public function setCharset(string $charset); public function getCharset(): ?string; + /** + * @return void + */ public function setLanguage(string $lang); public function getLanguage(): ?string; public function getName(): string; + /** + * @return void + */ public function setMaxLineLength(int $lineLength); public function getMaxLineLength(): int; diff --git a/symfony/mime/Header/Headers.php b/symfony/mime/Header/Headers.php index 8db912520..164f4ac33 100644 --- a/symfony/mime/Header/Headers.php +++ b/symfony/mime/Header/Headers.php @@ -34,16 +34,16 @@ final class Headers 'cc' => MailboxListHeader::class, 'bcc' => MailboxListHeader::class, 'message-id' => IdentificationHeader::class, - 'in-reply-to' => UnstructuredHeader::class, // `In-Reply-To` and `References` are less strict than RFC 2822 (3.6.4) to allow users entering the original email's ... - 'references' => UnstructuredHeader::class, // ... `Message-ID`, even if that is no valid `msg-id` + 'in-reply-to' => [UnstructuredHeader::class, IdentificationHeader::class], // `In-Reply-To` and `References` are less strict than RFC 2822 (3.6.4) to allow users entering the original email's ... + 'references' => [UnstructuredHeader::class, IdentificationHeader::class], // ... `Message-ID`, even if that is no valid `msg-id` 'return-path' => PathHeader::class, ]; /** * @var HeaderInterface[][] */ - private $headers = []; - private $lineLength = 76; + private array $headers = []; + private int $lineLength = 76; public function __construct(HeaderInterface ...$headers) { @@ -61,7 +61,7 @@ public function __clone() } } - public function setMaxLineLength(int $lineLength) + public function setMaxLineLength(int $lineLength): void { $this->lineLength = $lineLength; foreach ($this->all() as $header) { @@ -79,37 +79,31 @@ public function getMaxLineLength(): int * * @return $this */ - public function addMailboxListHeader(string $name, array $addresses): self + public function addMailboxListHeader(string $name, array $addresses): static { return $this->add(new MailboxListHeader($name, Address::createArray($addresses))); } /** - * @param Address|string $address - * * @return $this */ - public function addMailboxHeader(string $name, $address): self + public function addMailboxHeader(string $name, Address|string $address): static { return $this->add(new MailboxHeader($name, Address::create($address))); } /** - * @param string|array $ids - * * @return $this */ - public function addIdHeader(string $name, $ids): self + public function addIdHeader(string $name, string|array $ids): static { return $this->add(new IdentificationHeader($name, $ids)); } /** - * @param Address|string $path - * * @return $this */ - public function addPathHeader(string $name, $path): self + public function addPathHeader(string $name, Address|string $path): static { return $this->add(new PathHeader($name, $path instanceof Address ? $path : new Address($path))); } @@ -117,7 +111,7 @@ public function addPathHeader(string $name, $path): self /** * @return $this */ - public function addDateHeader(string $name, \DateTimeInterface $dateTime): self + public function addDateHeader(string $name, \DateTimeInterface $dateTime): static { return $this->add(new DateHeader($name, $dateTime)); } @@ -125,7 +119,7 @@ public function addDateHeader(string $name, \DateTimeInterface $dateTime): self /** * @return $this */ - public function addTextHeader(string $name, string $value): self + public function addTextHeader(string $name, string $value): static { return $this->add(new UnstructuredHeader($name, $value)); } @@ -133,7 +127,7 @@ public function addTextHeader(string $name, string $value): self /** * @return $this */ - public function addParameterizedHeader(string $name, string $value, array $params = []): self + public function addParameterizedHeader(string $name, string $value, array $params = []): static { return $this->add(new ParameterizedHeader($name, $value, $params)); } @@ -141,14 +135,20 @@ public function addParameterizedHeader(string $name, string $value, array $param /** * @return $this */ - public function addHeader(string $name, $argument, array $more = []): self + public function addHeader(string $name, mixed $argument, array $more = []): static { - $parts = explode('\\', self::HEADER_CLASS_MAP[strtolower($name)] ?? UnstructuredHeader::class); + $headerClass = self::HEADER_CLASS_MAP[strtolower($name)] ?? UnstructuredHeader::class; + if (\is_array($headerClass)) { + $headerClass = $headerClass[0]; + } + $parts = explode('\\', $headerClass); $method = 'add'.ucfirst(array_pop($parts)); if ('addUnstructuredHeader' === $method) { $method = 'addTextHeader'; } elseif ('addIdentificationHeader' === $method) { $method = 'addIdHeader'; + } elseif ('addMailboxListHeader' === $method && !\is_array($argument)) { + $argument = [$argument]; } return $this->$method($name, $argument, $more); @@ -162,7 +162,7 @@ public function has(string $name): bool /** * @return $this */ - public function add(HeaderInterface $header): self + public function add(HeaderInterface $header): static { self::checkHeaderClass($header); @@ -190,7 +190,7 @@ public function get(string $name): ?HeaderInterface return array_shift($values); } - public function all(string $name = null): iterable + public function all(?string $name = null): iterable { if (null === $name) { foreach ($this->headers as $name => $collection) { @@ -226,10 +226,22 @@ public static function isUniqueHeader(string $name): bool public static function checkHeaderClass(HeaderInterface $header): void { $name = strtolower($header->getName()); + $headerClasses = self::HEADER_CLASS_MAP[$name] ?? []; + if (!\is_array($headerClasses)) { + $headerClasses = [$headerClasses]; + } + + if (!$headerClasses) { + return; + } - if (($c = self::HEADER_CLASS_MAP[$name] ?? null) && !$header instanceof $c) { - throw new LogicException(sprintf('The "%s" header must be an instance of "%s" (got "%s").', $header->getName(), $c, get_debug_type($header))); + foreach ($headerClasses as $c) { + if ($header instanceof $c) { + return; + } } + + throw new LogicException(sprintf('The "%s" header must be an instance of "%s" (got "%s").', $header->getName(), implode('" or "', $headerClasses), get_debug_type($header))); } public function toString(): string @@ -254,10 +266,7 @@ public function toArray(): array return $arr; } - /** - * @internal - */ - public function getHeaderBody(string $name) + public function getHeaderBody(string $name): mixed { return $this->has($name) ? $this->get($name)->getBody() : null; } @@ -265,7 +274,7 @@ public function getHeaderBody(string $name) /** * @internal */ - public function setHeaderBody(string $type, string $name, $body): void + public function setHeaderBody(string $type, string $name, mixed $body): void { if ($this->has($name)) { $this->get($name)->setBody($body); diff --git a/symfony/mime/Header/IdentificationHeader.php b/symfony/mime/Header/IdentificationHeader.php index 8a94574e5..14e18bf25 100644 --- a/symfony/mime/Header/IdentificationHeader.php +++ b/symfony/mime/Header/IdentificationHeader.php @@ -21,13 +21,10 @@ */ final class IdentificationHeader extends AbstractHeader { - private $ids = []; - private $idsAsAddresses = []; + private array $ids = []; + private array $idsAsAddresses = []; - /** - * @param string|array $ids - */ - public function __construct(string $name, $ids) + public function __construct(string $name, string|array $ids) { parent::__construct($name); @@ -35,11 +32,11 @@ public function __construct(string $name, $ids) } /** - * @param string|array $body a string ID or an array of IDs + * @param string|string[] $body a string ID or an array of IDs * * @throws RfcComplianceException */ - public function setBody($body) + public function setBody(mixed $body): void { $this->setId($body); } @@ -52,11 +49,11 @@ public function getBody(): array /** * Set the ID used in the value of this header. * - * @param string|array $id + * @param string|string[] $id * * @throws RfcComplianceException */ - public function setId($id) + public function setId(string|array $id): void { $this->setIds(\is_array($id) ? $id : [$id]); } @@ -78,7 +75,7 @@ public function getId(): ?string * * @throws RfcComplianceException */ - public function setIds(array $ids) + public function setIds(array $ids): void { $this->ids = []; $this->idsAsAddresses = []; diff --git a/symfony/mime/Header/MailboxHeader.php b/symfony/mime/Header/MailboxHeader.php index b58c8252f..8ba964b53 100644 --- a/symfony/mime/Header/MailboxHeader.php +++ b/symfony/mime/Header/MailboxHeader.php @@ -21,7 +21,7 @@ */ final class MailboxHeader extends AbstractHeader { - private $address; + private Address $address; public function __construct(string $name, Address $address) { @@ -35,7 +35,7 @@ public function __construct(string $name, Address $address) * * @throws RfcComplianceException */ - public function setBody($body) + public function setBody(mixed $body): void { $this->setAddress($body); } @@ -51,7 +51,7 @@ public function getBody(): Address /** * @throws RfcComplianceException */ - public function setAddress(Address $address) + public function setAddress(Address $address): void { $this->address = $address; } diff --git a/symfony/mime/Header/MailboxListHeader.php b/symfony/mime/Header/MailboxListHeader.php index 1d00fdb12..8d902fb75 100644 --- a/symfony/mime/Header/MailboxListHeader.php +++ b/symfony/mime/Header/MailboxListHeader.php @@ -21,7 +21,7 @@ */ final class MailboxListHeader extends AbstractHeader { - private $addresses = []; + private array $addresses = []; /** * @param Address[] $addresses @@ -38,15 +38,15 @@ public function __construct(string $name, array $addresses) * * @throws RfcComplianceException */ - public function setBody($body) + public function setBody(mixed $body): void { $this->setAddresses($body); } /** - * @throws RfcComplianceException - * * @return Address[] + * + * @throws RfcComplianceException */ public function getBody(): array { @@ -60,7 +60,7 @@ public function getBody(): array * * @throws RfcComplianceException */ - public function setAddresses(array $addresses) + public function setAddresses(array $addresses): void { $this->addresses = []; $this->addAddresses($addresses); @@ -73,7 +73,7 @@ public function setAddresses(array $addresses) * * @throws RfcComplianceException */ - public function addAddresses(array $addresses) + public function addAddresses(array $addresses): void { foreach ($addresses as $address) { $this->addAddress($address); @@ -83,7 +83,7 @@ public function addAddresses(array $addresses) /** * @throws RfcComplianceException */ - public function addAddress(Address $address) + public function addAddress(Address $address): void { $this->addresses[] = $address; } @@ -99,9 +99,9 @@ public function getAddresses(): array /** * Gets the full mailbox list of this Header as an array of valid RFC 2822 strings. * - * @throws RfcComplianceException - * * @return string[] + * + * @throws RfcComplianceException */ public function getAddressStrings(): array { diff --git a/symfony/mime/Header/ParameterizedHeader.php b/symfony/mime/Header/ParameterizedHeader.php index e5d4238b4..5ef4f2120 100644 --- a/symfony/mime/Header/ParameterizedHeader.php +++ b/symfony/mime/Header/ParameterizedHeader.php @@ -25,8 +25,8 @@ final class ParameterizedHeader extends UnstructuredHeader */ public const TOKEN_REGEX = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)'; - private $encoder; - private $parameters = []; + private ?Rfc2231Encoder $encoder = null; + private array $parameters = []; public function __construct(string $name, string $value, array $parameters = []) { @@ -41,7 +41,7 @@ public function __construct(string $name, string $value, array $parameters = []) } } - public function setParameter(string $parameter, ?string $value) + public function setParameter(string $parameter, ?string $value): void { $this->setParameters(array_merge($this->getParameters(), [$parameter => $value])); } @@ -54,7 +54,7 @@ public function getParameter(string $parameter): string /** * @param string[] $parameters */ - public function setParameters(array $parameters) + public function setParameters(array $parameters): void { $this->parameters = $parameters; } @@ -85,7 +85,7 @@ public function getBodyAsString(): string * This doesn't need to be overridden in theory, but it is for implementation * reasons to prevent potential breakage of attributes. */ - protected function toTokens(string $string = null): array + protected function toTokens(?string $string = null): array { $tokens = parent::toTokens(parent::getBodyAsString()); diff --git a/symfony/mime/Header/PathHeader.php b/symfony/mime/Header/PathHeader.php index 5101ad0f9..63eb30af0 100644 --- a/symfony/mime/Header/PathHeader.php +++ b/symfony/mime/Header/PathHeader.php @@ -21,7 +21,7 @@ */ final class PathHeader extends AbstractHeader { - private $address; + private Address $address; public function __construct(string $name, Address $address) { @@ -35,7 +35,7 @@ public function __construct(string $name, Address $address) * * @throws RfcComplianceException */ - public function setBody($body) + public function setBody(mixed $body): void { $this->setAddress($body); } @@ -45,7 +45,7 @@ public function getBody(): Address return $this->getAddress(); } - public function setAddress(Address $address) + public function setAddress(Address $address): void { $this->address = $address; } diff --git a/symfony/mime/Header/UnstructuredHeader.php b/symfony/mime/Header/UnstructuredHeader.php index 2085ddfde..61c06d8f5 100644 --- a/symfony/mime/Header/UnstructuredHeader.php +++ b/symfony/mime/Header/UnstructuredHeader.php @@ -18,7 +18,7 @@ */ class UnstructuredHeader extends AbstractHeader { - private $value; + private string $value; public function __construct(string $name, string $value) { @@ -29,16 +29,15 @@ public function __construct(string $name, string $value) /** * @param string $body + * + * @return void */ - public function setBody($body) + public function setBody(mixed $body) { $this->setValue($body); } - /** - * @return string - */ - public function getBody() + public function getBody(): string { return $this->getValue(); } @@ -53,6 +52,8 @@ public function getValue(): string /** * Set the (unencoded) value of this header. + * + * @return void */ public function setValue(string $value) { diff --git a/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php b/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php new file mode 100644 index 000000000..2aaf8e6c4 --- /dev/null +++ b/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +/** + * @author Fabien Potencier + */ +class DefaultHtmlToTextConverter implements HtmlToTextConverterInterface +{ + public function convert(string $html, string $charset): string + { + return strip_tags(preg_replace('{<(head|style)\b.*?}is', '', $html)); + } +} diff --git a/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php b/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php new file mode 100644 index 000000000..696f37ccf --- /dev/null +++ b/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +/** + * @author Fabien Potencier + */ +interface HtmlToTextConverterInterface +{ + /** + * Converts an HTML representation of a Message to a text representation. + * + * The output must use the same charset as the HTML one. + */ + public function convert(string $html, string $charset): string; +} diff --git a/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php b/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php new file mode 100644 index 000000000..253a7b19f --- /dev/null +++ b/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +use League\HTMLToMarkdown\HtmlConverter; +use League\HTMLToMarkdown\HtmlConverterInterface; + +/** + * @author Fabien Potencier + */ +class LeagueHtmlToMarkdownConverter implements HtmlToTextConverterInterface +{ + public function __construct( + private HtmlConverterInterface $converter = new HtmlConverter([ + 'hard_break' => true, + 'strip_tags' => true, + 'remove_nodes' => 'head style', + ]), + ) { + } + + public function convert(string $html, string $charset): string + { + return $this->converter->convert($html); + } +} diff --git a/symfony/mime/LICENSE b/symfony/mime/LICENSE index 58b42bc8a..4dd83ce0f 100644 --- a/symfony/mime/LICENSE +++ b/symfony/mime/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2023 Fabien Potencier +Copyright (c) 2010-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/mime/Message.php b/symfony/mime/Message.php index 651ffd452..fc8940ebd 100644 --- a/symfony/mime/Message.php +++ b/symfony/mime/Message.php @@ -21,10 +21,10 @@ */ class Message extends RawMessage { - private $headers; - private $body; + private Headers $headers; + private ?AbstractPart $body; - public function __construct(Headers $headers = null, AbstractPart $body = null) + public function __construct(?Headers $headers = null, ?AbstractPart $body = null) { $this->headers = $headers ? clone $headers : new Headers(); $this->body = $body; @@ -42,8 +42,11 @@ public function __clone() /** * @return $this */ - public function setBody(AbstractPart $body = null) + public function setBody(?AbstractPart $body = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/mime', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->body = $body; return $this; @@ -57,7 +60,7 @@ public function getBody(): ?AbstractPart /** * @return $this */ - public function setHeaders(Headers $headers) + public function setHeaders(Headers $headers): static { $this->headers = $headers; @@ -122,13 +125,16 @@ public function toIterable(): iterable yield from $body->toIterable(); } + /** + * @return void + */ public function ensureValidity() { - if (!$this->headers->has('To') && !$this->headers->has('Cc') && !$this->headers->has('Bcc')) { + if (!$this->headers->get('To')?->getBody() && !$this->headers->get('Cc')?->getBody() && !$this->headers->get('Bcc')?->getBody()) { throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.'); } - if (!$this->headers->has('From') && !$this->headers->has('Sender')) { + if (!$this->headers->get('From')?->getBody() && !$this->headers->get('Sender')?->getBody()) { throw new LogicException('An email must have a "From" or a "Sender" header.'); } @@ -140,7 +146,10 @@ public function generateMessageId(): string if ($this->headers->has('Sender')) { $sender = $this->headers->get('Sender')->getAddress(); } elseif ($this->headers->has('From')) { - $sender = $this->headers->get('From')->getAddresses()[0]; + if (!$froms = $this->headers->get('From')->getAddresses()) { + throw new LogicException('A "From" header must have at least one email address.'); + } + $sender = $froms[0]; } else { throw new LogicException('An email must have a "From" or a "Sender" header.'); } diff --git a/symfony/mime/MessageConverter.php b/symfony/mime/MessageConverter.php index 0539eac8e..bdce921af 100644 --- a/symfony/mime/MessageConverter.php +++ b/symfony/mime/MessageConverter.php @@ -58,7 +58,7 @@ public static function toEmail(Message $message): Email throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); } - return self::attachParts($email, \array_slice($parts, 1)); + return self::addParts($email, \array_slice($parts, 1)); } throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); @@ -80,9 +80,9 @@ private static function createEmailFromAlternativePart(Message $message, Alterna { $parts = $part->getParts(); if ( - 2 === \count($parts) && - $parts[0] instanceof TextPart && 'text' === $parts[0]->getMediaType() && 'plain' === $parts[0]->getMediaSubtype() && - $parts[1] instanceof TextPart && 'text' === $parts[1]->getMediaType() && 'html' === $parts[1]->getMediaSubtype() + 2 === \count($parts) + && $parts[0] instanceof TextPart && 'text' === $parts[0]->getMediaType() && 'plain' === $parts[0]->getMediaSubtype() + && $parts[1] instanceof TextPart && 'text' === $parts[1]->getMediaType() && 'html' === $parts[1]->getMediaSubtype() ) { return (new Email(clone $message->getHeaders())) ->text($parts[0]->getBody(), $parts[0]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8') @@ -104,20 +104,17 @@ private static function createEmailFromRelatedPart(Message $message, RelatedPart throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); } - return self::attachParts($email, \array_slice($parts, 1)); + return self::addParts($email, \array_slice($parts, 1)); } - private static function attachParts(Email $email, array $parts): Email + private static function addParts(Email $email, array $parts): Email { foreach ($parts as $part) { if (!$part instanceof DataPart) { throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($email))); } - $headers = $part->getPreparedHeaders(); - $method = 'inline' === $headers->getHeaderBody('Content-Disposition') ? 'embed' : 'attach'; - $name = $headers->getHeaderParameter('Content-Disposition', 'filename'); - $email->$method($part->getBody(), $name, $part->getMediaType().'/'.$part->getMediaSubtype()); + $email->addPart($part); } return $email; diff --git a/symfony/mime/MimeTypes.php b/symfony/mime/MimeTypes.php index bdea994b5..19628b0b1 100644 --- a/symfony/mime/MimeTypes.php +++ b/symfony/mime/MimeTypes.php @@ -36,14 +36,14 @@ */ final class MimeTypes implements MimeTypesInterface { - private $extensions = []; - private $mimeTypes = []; + private array $extensions = []; + private array $mimeTypes = []; /** * @var MimeTypeGuesserInterface[] */ - private $guessers = []; - private static $default; + private array $guessers = []; + private static MimeTypes $default; public function __construct(array $map = []) { @@ -58,14 +58,14 @@ public function __construct(array $map = []) $this->registerGuesser(new FileinfoMimeTypeGuesser()); } - public static function setDefault(self $default) + public static function setDefault(self $default): void { self::$default = $default; } public static function getDefault(): self { - return self::$default ?? self::$default = new self(); + return self::$default ??= new self(); } /** @@ -73,14 +73,11 @@ public static function getDefault(): self * * The last registered guesser has precedence over the other ones. */ - public function registerGuesser(MimeTypeGuesserInterface $guesser) + public function registerGuesser(MimeTypeGuesserInterface $guesser): void { array_unshift($this->guessers, $guesser); } - /** - * {@inheritdoc} - */ public function getExtensions(string $mimeType): array { if ($this->extensions) { @@ -90,9 +87,6 @@ public function getExtensions(string $mimeType): array return $extensions ?? self::MAP[$mimeType] ?? self::MAP[$lcMimeType ?? strtolower($mimeType)] ?? []; } - /** - * {@inheritdoc} - */ public function getMimeTypes(string $ext): array { if ($this->mimeTypes) { @@ -102,9 +96,6 @@ public function getMimeTypes(string $ext): array return $mimeTypes ?? self::REVERSE_MAP[$ext] ?? self::REVERSE_MAP[$lcExt ?? strtolower($ext)] ?? []; } - /** - * {@inheritdoc} - */ public function isGuesserSupported(): bool { foreach ($this->guessers as $guesser) { @@ -117,8 +108,6 @@ public function isGuesserSupported(): bool } /** - * {@inheritdoc} - * * The file is passed to each registered MIME type guesser in reverse order * of their registration (last registered is queried first). Once a guesser * returns a value that is not null, this method terminates and returns the @@ -146,7 +135,7 @@ public function guessMimeType(string $path): ?string /** * A map of MIME types and their default extensions. * - * Updated from upstream on 2021-09-03 + * Updated from upstream on 2023-10-14. * * @see Resources/bin/update_mime_types.php */ @@ -162,9 +151,11 @@ public function guessMimeType(string $path): ?string 'application/atsc-dwd+xml' => ['dwd'], 'application/atsc-held+xml' => ['held'], 'application/atsc-rsat+xml' => ['rsat'], + 'application/bat' => ['bat'], 'application/bdoc' => ['bdoc'], 'application/bzip2' => ['bz2', 'bz'], 'application/calendar+xml' => ['xcs'], + 'application/cbor' => ['cbor'], 'application/ccxml+xml' => ['ccxml'], 'application/cdfx+xml' => ['cdfx'], 'application/cdmi-capability' => ['cdmia'], @@ -174,9 +165,11 @@ public function guessMimeType(string $path): ?string 'application/cdmi-queue' => ['cdmiq'], 'application/cdr' => ['cdr'], 'application/coreldraw' => ['cdr'], + 'application/cpl+xml' => ['cpl'], 'application/csv' => ['csv'], 'application/cu-seeme' => ['cu'], 'application/dash+xml' => ['mpd'], + 'application/dash-patch+xml' => ['mpp'], 'application/davmount+xml' => ['davmount'], 'application/dbase' => ['dbf'], 'application/dbf' => ['dbf'], @@ -190,7 +183,9 @@ public function guessMimeType(string $path): ?string 'application/emotionml+xml' => ['emotionml'], 'application/epub+zip' => ['epub'], 'application/exi' => ['exi'], + 'application/express' => ['exp'], 'application/fdt+xml' => ['fdt'], + 'application/fits' => ['fits', 'fit', 'fts'], 'application/font-tdpfr' => ['pfr'], 'application/font-woff' => ['woff'], 'application/futuresplash' => ['swf', 'spl'], @@ -235,6 +230,7 @@ public function guessMimeType(string $path): ?string 'application/mathml+xml' => ['mathml', 'mml'], 'application/mbox' => ['mbox'], 'application/mdb' => ['mdb'], + 'application/media-policy-dataset+xml' => ['mpf'], 'application/mediaservercontrol+xml' => ['mscml'], 'application/metalink+xml' => ['metalink'], 'application/metalink4+xml' => ['meta4'], @@ -272,7 +268,7 @@ public function guessMimeType(string $path): ?string 'application/pdf' => ['pdf'], 'application/pgp' => ['pgp', 'gpg', 'asc'], 'application/pgp-encrypted' => ['pgp', 'gpg', 'asc'], - 'application/pgp-keys' => ['skr', 'pkr', 'asc', 'pgp', 'gpg', 'key'], + 'application/pgp-keys' => ['asc', 'skr', 'pkr', 'pgp', 'gpg', 'key'], 'application/pgp-signature' => ['asc', 'sig', 'pgp', 'gpg'], 'application/photoshop' => ['psd'], 'application/pics-rules' => ['prf'], @@ -326,7 +322,7 @@ public function guessMimeType(string $path): ?string 'application/sieve' => ['siv', 'sieve'], 'application/smil' => ['smil', 'smi', 'sml', 'kino'], 'application/smil+xml' => ['smi', 'smil', 'sml', 'kino'], - 'application/sparql-query' => ['rq'], + 'application/sparql-query' => ['rq', 'qs'], 'application/sparql-results+xml' => ['srx'], 'application/sql' => ['sql'], 'application/srgs' => ['gram'], @@ -363,6 +359,7 @@ public function guessMimeType(string $path): ?string 'application/vnd.adobe.illustrator' => ['ai'], 'application/vnd.adobe.xdp+xml' => ['xdp'], 'application/vnd.adobe.xfdf' => ['xfdf'], + 'application/vnd.age' => ['age'], 'application/vnd.ahead.space' => ['ahead'], 'application/vnd.airzip.filesecure.azf' => ['azf'], 'application/vnd.airzip.filesecure.azs' => ['azs'], @@ -433,6 +430,8 @@ public function guessMimeType(string $path): ?string 'application/vnd.dvb.service' => ['svc'], 'application/vnd.dynageo' => ['geo'], 'application/vnd.ecowin.chart' => ['mag'], + 'application/vnd.efi.img' => ['raw-disk-image', 'img'], + 'application/vnd.efi.iso' => ['iso', 'iso9660'], 'application/vnd.emusic-emusic_package' => ['emp'], 'application/vnd.enliven' => ['nml'], 'application/vnd.epson.esf' => ['esf'], @@ -473,6 +472,7 @@ public function guessMimeType(string $path): ?string 'application/vnd.geonext' => ['gxt'], 'application/vnd.geoplan' => ['g2w'], 'application/vnd.geospace' => ['g3w'], + 'application/vnd.gerber' => ['gbr'], 'application/vnd.gmx' => ['gmx'], 'application/vnd.google-apps.document' => ['gdoc'], 'application/vnd.google-apps.presentation' => ['gslides'], @@ -565,6 +565,7 @@ public function guessMimeType(string $path): ?string 'application/vnd.mophun.application' => ['mpn'], 'application/vnd.mophun.certificate' => ['mpc'], 'application/vnd.mozilla.xul+xml' => ['xul'], + 'application/vnd.ms-3mfdocument' => ['3mf'], 'application/vnd.ms-access' => ['mdb'], 'application/vnd.ms-artgalry' => ['cil'], 'application/vnd.ms-asf' => ['asf'], @@ -737,6 +738,7 @@ public function guessMimeType(string $path): ?string 'application/vnd.trid.tpt' => ['tpt'], 'application/vnd.triscape.mxs' => ['mxs'], 'application/vnd.trueapp' => ['tra'], + 'application/vnd.truedoc' => ['pfr'], 'application/vnd.ufdl' => ['ufd', 'ufdl'], 'application/vnd.uiq.theme' => ['utz'], 'application/vnd.umajin' => ['umj'], @@ -770,6 +772,7 @@ public function guessMimeType(string $path): ?string 'application/vnd.zzazz.deck+xml' => ['zaz'], 'application/voicexml+xml' => ['vxml'], 'application/wasm' => ['wasm'], + 'application/watcherinfo+xml' => ['wif'], 'application/widget' => ['wgt'], 'application/winhlp' => ['hlp'], 'application/wk1' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], @@ -795,6 +798,7 @@ public function guessMimeType(string $path): ?string 'application/x-applix-word' => ['aw'], 'application/x-archive' => ['a', 'ar'], 'application/x-arj' => ['arj'], + 'application/x-asar' => ['asar'], 'application/x-asp' => ['asp'], 'application/x-atari-2600-rom' => ['a26'], 'application/x-atari-7800-rom' => ['a78'], @@ -803,18 +807,22 @@ public function guessMimeType(string $path): ?string 'application/x-authorware-map' => ['aam'], 'application/x-authorware-seg' => ['aas'], 'application/x-awk' => ['awk'], + 'application/x-bat' => ['bat'], 'application/x-bcpio' => ['bcpio'], 'application/x-bdoc' => ['bdoc'], 'application/x-bittorrent' => ['torrent'], - 'application/x-blender' => ['blender', 'blend', 'BLEND'], + 'application/x-blender' => ['blend', 'BLEND', 'blender'], 'application/x-blorb' => ['blb', 'blorb'], 'application/x-bps-patch' => ['bps'], 'application/x-bsdiff' => ['bsdiff'], 'application/x-bz2' => ['bz2'], 'application/x-bzdvi' => ['dvi.bz2'], - 'application/x-bzip' => ['bz', 'bz2'], - 'application/x-bzip-compressed-tar' => ['tar.bz2', 'tar.bz', 'tbz2', 'tbz', 'tb2'], - 'application/x-bzip2' => ['bz2', 'boz', 'bz'], + 'application/x-bzip' => ['bz'], + 'application/x-bzip-compressed-tar' => ['tar.bz', 'tbz', 'tbz2', 'tb2'], + 'application/x-bzip2' => ['bz2', 'boz'], + 'application/x-bzip2-compressed-tar' => ['tar.bz2', 'tbz2', 'tb2'], + 'application/x-bzip3' => ['bz3'], + 'application/x-bzip3-compressed-tar' => ['tar.bz3', 'tbz3'], 'application/x-bzpdf' => ['pdf.bz2'], 'application/x-bzpostscript' => ['ps.bz2'], 'application/x-cb7' => ['cb7'], @@ -867,11 +875,14 @@ public function guessMimeType(string $path): ?string 'application/x-egon' => ['egon'], 'application/x-emf' => ['emf'], 'application/x-envoy' => ['evy'], + 'application/x-eris-link+cbor' => ['eris'], 'application/x-eva' => ['eva'], + 'application/x-excellon' => ['drl'], 'application/x-fd-file' => ['fd', 'qd'], 'application/x-fds-disk' => ['fds'], 'application/x-fictionbook' => ['fb2'], 'application/x-fictionbook+xml' => ['fb2'], + 'application/x-fishscript' => ['fish'], 'application/x-flash-video' => ['flv'], 'application/x-fluid' => ['fl'], 'application/x-font-afm' => ['afm'], @@ -898,9 +909,12 @@ public function guessMimeType(string $path): ?string 'application/x-gba-rom' => ['gba', 'agb'], 'application/x-gca-compressed' => ['gca'], 'application/x-gd-rom-cue' => ['gdi'], + 'application/x-gdscript' => ['gd'], 'application/x-gedcom' => ['ged', 'gedcom'], 'application/x-genesis-32x-rom' => ['32x', 'mdx'], 'application/x-genesis-rom' => ['gen', 'smd', 'sgd'], + 'application/x-gerber' => ['gbr'], + 'application/x-gerber-job' => ['gbrjob'], 'application/x-gettext' => ['po'], 'application/x-gettext-translation' => ['gmo', 'mo'], 'application/x-glade' => ['glade'], @@ -910,6 +924,9 @@ public function guessMimeType(string $path): ?string 'application/x-gnumeric' => ['gnumeric'], 'application/x-gnuplot' => ['gp', 'gplt', 'gnuplot'], 'application/x-go-sgf' => ['sgf'], + 'application/x-godot-resource' => ['res', 'tres'], + 'application/x-godot-scene' => ['scn', 'tscn', 'escn'], + 'application/x-godot-shader' => ['gdshader'], 'application/x-gpx' => ['gpx'], 'application/x-gpx+xml' => ['gpx'], 'application/x-gramps-xml' => ['gramps'], @@ -971,6 +988,7 @@ public function guessMimeType(string $path): ?string 'application/x-lha' => ['lha', 'lzh'], 'application/x-lhz' => ['lhz'], 'application/x-linguist' => ['ts'], + 'application/x-lmdb' => ['mdb', 'lmdb'], 'application/x-lotus123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], 'application/x-lrzip' => ['lrz'], 'application/x-lrzip-compressed-tar' => ['tar.lrz', 'tlrz'], @@ -997,9 +1015,11 @@ public function guessMimeType(string $path): ?string 'application/x-mimearchive' => ['mhtml', 'mht'], 'application/x-mobi8-ebook' => ['azw3', 'kfx'], 'application/x-mobipocket-ebook' => ['prc', 'mobi'], + 'application/x-modrinth-modpack+zip' => ['mrpack'], 'application/x-ms-application' => ['application'], 'application/x-ms-asx' => ['asx', 'wax', 'wvx', 'wmx'], 'application/x-ms-dos-executable' => ['exe'], + 'application/x-ms-pdb' => ['pdb'], 'application/x-ms-shortcut' => ['lnk'], 'application/x-ms-wim' => ['wim', 'swm'], 'application/x-ms-wmd' => ['wmd'], @@ -1035,10 +1055,13 @@ public function guessMimeType(string $path): ?string 'application/x-nintendo-3ds-rom' => ['3ds', 'cci'], 'application/x-nintendo-ds-rom' => ['nds'], 'application/x-ns-proxy-autoconfig' => ['pac'], + 'application/x-nuscript' => ['nu'], 'application/x-nzb' => ['nzb'], 'application/x-object' => ['o', 'mod'], 'application/x-ogg' => ['ogx'], 'application/x-oleo' => ['oleo'], + 'application/x-openvpn-profile' => ['openvpn', 'ovpn'], + 'application/x-openzim' => ['zim'], 'application/x-pagemaker' => ['p65', 'pm', 'pm6', 'pmd'], 'application/x-pak' => ['pak'], 'application/x-palm-database' => ['prc', 'pdb', 'pqa', 'oprc'], @@ -1097,6 +1120,7 @@ public function guessMimeType(string $path): ?string 'application/x-siag' => ['siag'], 'application/x-silverlight-app' => ['xap'], 'application/x-sit' => ['sit'], + 'application/x-sitx' => ['sitx'], 'application/x-smaf' => ['mmf', 'smaf'], 'application/x-sms-rom' => ['sms'], 'application/x-snes-rom' => ['sfc', 'smc'], @@ -1131,6 +1155,8 @@ public function guessMimeType(string $path): ?string 'application/x-thomson-cartridge-memo7' => ['m7'], 'application/x-thomson-cassette' => ['k7'], 'application/x-thomson-sap-image' => ['sap'], + 'application/x-tiled-tmx' => ['tmx'], + 'application/x-tiled-tsx' => ['tsx'], 'application/x-trash' => ['bak', 'old', 'sik'], 'application/x-trig' => ['trig'], 'application/x-troff' => ['tr', 'roff', 't'], @@ -1179,11 +1205,12 @@ public function guessMimeType(string $path): ?string 'application/x-xz-compressed-tar' => ['tar.xz', 'txz'], 'application/x-xzpdf' => ['pdf.xz'], 'application/x-yaml' => ['yaml', 'yml'], - 'application/x-zip' => ['zip'], - 'application/x-zip-compressed' => ['zip'], + 'application/x-zip' => ['zip', 'zipx'], + 'application/x-zip-compressed' => ['zip', 'zipx'], 'application/x-zip-compressed-fb2' => ['fb2.zip'], 'application/x-zmachine' => ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'], 'application/x-zoo' => ['zoo'], + 'application/x-zpaq' => ['zpaq'], 'application/x-zstd-compressed-tar' => ['tar.zst', 'tzst'], 'application/xaml+xml' => ['xaml'], 'application/xcap-att+xml' => ['xav'], @@ -1204,9 +1231,10 @@ public function guessMimeType(string $path): ?string 'application/xslt+xml' => ['xsl', 'xslt'], 'application/xspf+xml' => ['xspf'], 'application/xv+xml' => ['mxml', 'xhvml', 'xvml', 'xvm'], + 'application/yaml' => ['yaml', 'yml'], 'application/yang' => ['yang'], 'application/yin+xml' => ['yin'], - 'application/zip' => ['zip'], + 'application/zip' => ['zip', 'zipx'], 'application/zlib' => ['zz'], 'application/zstd' => ['zst'], 'audio/3gpp' => ['3gpp', '3gp', '3ga'], @@ -1221,12 +1249,15 @@ public function guessMimeType(string $path): ?string 'audio/amr-wb-encrypted' => ['awb'], 'audio/annodex' => ['axa'], 'audio/basic' => ['au', 'snd'], + 'audio/dff' => ['dff'], + 'audio/dsd' => ['dsf'], + 'audio/dsf' => ['dsf'], 'audio/flac' => ['flac'], 'audio/imelody' => ['imy', 'ime'], 'audio/m3u' => ['m3u', 'm3u8', 'vlc'], 'audio/m4a' => ['m4a', 'f4a'], 'audio/midi' => ['mid', 'midi', 'kar', 'rmi'], - 'audio/mobile-xmf' => ['mxmf', 'xmf'], + 'audio/mobile-xmf' => ['mxmf'], 'audio/mp2' => ['mp2'], 'audio/mp3' => ['mp3', 'mpga'], 'audio/mp4' => ['m4a', 'mp4a', 'f4a'], @@ -1241,6 +1272,7 @@ public function guessMimeType(string $path): ?string 'audio/usac' => ['loas', 'xhe'], 'audio/vnd.audible' => ['aa', 'aax'], 'audio/vnd.audible.aax' => ['aax'], + 'audio/vnd.audible.aaxc' => ['aaxc'], 'audio/vnd.dece.audio' => ['uva', 'uvva'], 'audio/vnd.digital-winds' => ['eol'], 'audio/vnd.dra' => ['dra'], @@ -1249,6 +1281,7 @@ public function guessMimeType(string $path): ?string 'audio/vnd.lucent.voice' => ['lvp'], 'audio/vnd.m-realaudio' => ['ra', 'rax'], 'audio/vnd.ms-playready.media.pya' => ['pya'], + 'audio/vnd.nokia.mobile-xmf' => ['mxmf'], 'audio/vnd.nuera.ecelp4800' => ['ecelp4800'], 'audio/vnd.nuera.ecelp7470' => ['ecelp7470'], 'audio/vnd.nuera.ecelp9600' => ['ecelp9600'], @@ -1268,6 +1301,9 @@ public function guessMimeType(string $path): ?string 'audio/x-annodex' => ['axa'], 'audio/x-ape' => ['ape'], 'audio/x-caf' => ['caf'], + 'audio/x-dff' => ['dff'], + 'audio/x-dsd' => ['dsf'], + 'audio/x-dsf' => ['dsf'], 'audio/x-dts' => ['dts'], 'audio/x-dtshd' => ['dtshd'], 'audio/x-flac' => ['flac'], @@ -1316,6 +1352,7 @@ public function guessMimeType(string $path): ?string 'audio/x-speex' => ['spx'], 'audio/x-speex+ogg' => ['oga', 'ogg', 'spx'], 'audio/x-stm' => ['stm'], + 'audio/x-tak' => ['tak'], 'audio/x-tta' => ['tta'], 'audio/x-voc' => ['voc'], 'audio/x-vorbis' => ['oga', 'ogg'], @@ -1341,8 +1378,10 @@ public function guessMimeType(string $path): ?string 'font/woff' => ['woff'], 'font/woff2' => ['woff2'], 'image/aces' => ['exr'], - 'image/apng' => ['apng'], + 'image/apng' => ['apng', 'png'], 'image/astc' => ['astc'], + 'image/avci' => ['avci'], + 'image/avcs' => ['avcs'], 'image/avif' => ['avif', 'avifs'], 'image/avif-sequence' => ['avif', 'avifs'], 'image/bmp' => ['bmp', 'dib'], @@ -1351,13 +1390,13 @@ public function guessMimeType(string $path): ?string 'image/dicom-rle' => ['drle'], 'image/emf' => ['emf'], 'image/fax-g3' => ['g3'], - 'image/fits' => ['fits'], + 'image/fits' => ['fits', 'fit', 'fts'], 'image/g3fax' => ['g3'], 'image/gif' => ['gif'], - 'image/heic' => ['heic', 'heif'], - 'image/heic-sequence' => ['heics', 'heic', 'heif'], - 'image/heif' => ['heif', 'heic'], - 'image/heif-sequence' => ['heifs', 'heic', 'heif'], + 'image/heic' => ['heic', 'heif', 'hif'], + 'image/heic-sequence' => ['heics', 'heic', 'heif', 'hif'], + 'image/heif' => ['heif', 'heic', 'hif'], + 'image/heif-sequence' => ['heifs', 'heic', 'heif', 'hif'], 'image/hej2k' => ['hej2'], 'image/hsj2' => ['hsj2'], 'image/ico' => ['ico'], @@ -1373,7 +1412,7 @@ public function guessMimeType(string $path): ?string 'image/jpm' => ['jpm', 'jpgm'], 'image/jpx' => ['jpx', 'jpf'], 'image/jxl' => ['jxl'], - 'image/jxr' => ['jxr'], + 'image/jxr' => ['jxr', 'hdp', 'wdp'], 'image/jxra' => ['jxra'], 'image/jxrs' => ['jxrs'], 'image/jxs' => ['jxs'], @@ -1390,11 +1429,12 @@ public function guessMimeType(string $path): ?string 'image/prs.btif' => ['btif'], 'image/prs.pti' => ['pti'], 'image/psd' => ['psd'], + 'image/qoi' => ['qoi'], 'image/rle' => ['rle'], 'image/sgi' => ['sgi'], 'image/svg' => ['svg'], 'image/svg+xml' => ['svg', 'svgz'], - 'image/svg+xml-compressed' => ['svgz'], + 'image/svg+xml-compressed' => ['svgz', 'svg.gz'], 'image/t38' => ['t38'], 'image/targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], 'image/tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], @@ -1414,9 +1454,10 @@ public function guessMimeType(string $path): ?string 'image/vnd.fujixerox.edmics-mmr' => ['mmr'], 'image/vnd.fujixerox.edmics-rlc' => ['rlc'], 'image/vnd.microsoft.icon' => ['ico'], + 'image/vnd.mozilla.apng' => ['apng', 'png'], 'image/vnd.ms-dds' => ['dds'], 'image/vnd.ms-modi' => ['mdi'], - 'image/vnd.ms-photo' => ['wdp'], + 'image/vnd.ms-photo' => ['wdp', 'jxr', 'hdp'], 'image/vnd.net-fpx' => ['npx'], 'image/vnd.pco.b16' => ['b16'], 'image/vnd.rn-realpix' => ['rp'], @@ -1444,7 +1485,7 @@ public function guessMimeType(string $path): ?string 'image/x-emf' => ['emf'], 'image/x-eps' => ['eps', 'epsi', 'epsf'], 'image/x-exr' => ['exr'], - 'image/x-fits' => ['fits'], + 'image/x-fits' => ['fits', 'fit', 'fts'], 'image/x-freehand' => ['fh', 'fhc', 'fh4', 'fh5', 'fh7'], 'image/x-fuji-raf' => ['raf'], 'image/x-gimp-gbr' => ['gbr'], @@ -1522,6 +1563,7 @@ public function guessMimeType(string $path): ?string 'model/mesh' => ['msh', 'mesh', 'silo'], 'model/mtl' => ['mtl'], 'model/obj' => ['obj'], + 'model/step+xml' => ['stpx'], 'model/step+zip' => ['stpz'], 'model/step-xml+zip' => ['stpxz'], 'model/stl' => ['stl'], @@ -1561,6 +1603,7 @@ public function guessMimeType(string $path): ?string 'text/jade' => ['jade'], 'text/javascript' => ['js', 'jsm', 'mjs'], 'text/jsx' => ['jsx'], + 'text/julia' => ['jl'], 'text/less' => ['less'], 'text/markdown' => ['md', 'markdown', 'mkd'], 'text/mathml' => ['mml'], @@ -1593,6 +1636,7 @@ public function guessMimeType(string $path): ?string 'text/vnd.curl.mcurl' => ['mcurl'], 'text/vnd.curl.scurl' => ['scurl'], 'text/vnd.dvb.subtitle' => ['sub'], + 'text/vnd.familysearch.gedcom' => ['ged', 'gedcom'], 'text/vnd.fly' => ['fly'], 'text/vnd.fmi.flexstor' => ['flx'], 'text/vnd.graphviz' => ['gv', 'dot'], @@ -1609,6 +1653,7 @@ public function guessMimeType(string $path): ?string 'text/x-adasrc' => ['adb', 'ads'], 'text/x-asm' => ['s', 'asm'], 'text/x-bibtex' => ['bib'], + 'text/x-blueprint' => ['blp'], 'text/x-c' => ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'], 'text/x-c++hdr' => ['hh', 'hp', 'hpp', 'h++', 'hxx'], 'text/x-c++src' => ['cpp', 'cxx', 'cc', 'C', 'c++'], @@ -1625,6 +1670,8 @@ public function guessMimeType(string $path): ?string 'text/x-dart' => ['dart'], 'text/x-dbus-service' => ['service'], 'text/x-dcl' => ['dcl'], + 'text/x-devicetree-binary' => ['dtb'], + 'text/x-devicetree-source' => ['dts', 'dtsi'], 'text/x-diff' => ['diff', 'patch'], 'text/x-dsl' => ['dsl'], 'text/x-dsrc' => ['d', 'di'], @@ -1633,7 +1680,9 @@ public function guessMimeType(string $path): ?string 'text/x-elixir' => ['ex', 'exs'], 'text/x-emacs-lisp' => ['el'], 'text/x-erlang' => ['erl'], + 'text/x-fish' => ['fish'], 'text/x-fortran' => ['f', 'for', 'f77', 'f90', 'f95'], + 'text/x-gcode-gx' => ['gx'], 'text/x-genie' => ['gs'], 'text/x-gettext-translation' => ['po'], 'text/x-gettext-translation-template' => ['pot'], @@ -1664,11 +1713,16 @@ public function guessMimeType(string $path): ?string 'text/x-moc' => ['moc'], 'text/x-modelica' => ['mo'], 'text/x-mof' => ['mof'], + 'text/x-mpl2' => ['mpl'], 'text/x-mpsub' => ['sub'], 'text/x-mrml' => ['mrml', 'mrl'], 'text/x-ms-regedit' => ['reg'], 'text/x-mup' => ['mup', 'not'], 'text/x-nfo' => ['nfo'], + 'text/x-nim' => ['nim'], + 'text/x-nimscript' => ['nims', 'nimble'], + 'text/x-nu' => ['nu'], + 'text/x-objc++src' => ['mm'], 'text/x-objcsrc' => ['m'], 'text/x-ocaml' => ['ml', 'mli'], 'text/x-ocl' => ['ocl'], @@ -1714,6 +1768,7 @@ public function guessMimeType(string $path): ?string 'text/x-troff-ms' => ['ms'], 'text/x-twig' => ['twig'], 'text/x-txt2tags' => ['t2t'], + 'text/x-typst' => ['typ'], 'text/x-uil' => ['uil'], 'text/x-uuencode' => ['uu', 'uue'], 'text/x-vala' => ['vala', 'vapi'], @@ -1754,6 +1809,7 @@ public function guessMimeType(string $path): ?string 'video/ogg' => ['ogv', 'ogg'], 'video/quicktime' => ['mov', 'qt', 'moov', 'qtvr'], 'video/vivo' => ['viv', 'vivo'], + 'video/vnd.avi' => ['avi', 'avf', 'divx'], 'video/vnd.dece.hd' => ['uvh', 'uvvh'], 'video/vnd.dece.mobile' => ['uvm', 'uvvm'], 'video/vnd.dece.pd' => ['uvp', 'uvvp'], @@ -1769,8 +1825,9 @@ public function guessMimeType(string $path): ?string 'video/vnd.rn-realvideo' => ['rv', 'rvx'], 'video/vnd.uvvu.mp4' => ['uvu', 'uvvu'], 'video/vnd.vivo' => ['viv', 'vivo'], + 'video/vnd.youtube.yt' => ['yt'], 'video/webm' => ['webm'], - 'video/x-anim' => ['anim[1-9j]'], + 'video/x-anim' => ['anim1', 'anim2', 'anim3', 'anim4', 'anim5', 'anim6', 'anim7', 'anim8', 'anim9', 'animj'], 'video/x-annodex' => ['axv'], 'video/x-avi' => ['avi', 'avf', 'divx'], 'video/x-f4v' => ['f4v'], @@ -1818,6 +1875,7 @@ public function guessMimeType(string $path): ?string ]; private const REVERSE_MAP = [ + '123' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], '1km' => ['application/vnd.1000minds.decision-model+xml'], '32x' => ['application/x-genesis-32x-rom'], '3dml' => ['text/vnd.in3d.3dml'], @@ -1829,7 +1887,9 @@ public function guessMimeType(string $path): ?string '3gp2' => ['audio/3gpp2', 'video/3gpp2'], '3gpp' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], '3gpp2' => ['audio/3gpp2', 'video/3gpp2'], - '3mf' => ['model/3mf'], + '3mf' => ['application/vnd.ms-3mfdocument', 'model/3mf'], + '602' => ['application/x-t602'], + '669' => ['audio/x-mod'], '7z' => ['application/x-7z-compressed'], '7z.001' => ['application/x-7z-compressed'], 'BLEND' => ['application/x-blender'], @@ -1846,6 +1906,7 @@ public function guessMimeType(string $path): ?string 'aam' => ['application/x-authorware-map'], 'aas' => ['application/x-authorware-seg'], 'aax' => ['audio/vnd.audible', 'audio/vnd.audible.aax', 'audio/x-pn-audibleaudio'], + 'aaxc' => ['audio/vnd.audible.aaxc'], 'abw' => ['application/x-abiword'], 'abw.CRASHED' => ['application/x-abiword'], 'abw.gz' => ['application/x-abiword'], @@ -1865,6 +1926,7 @@ public function guessMimeType(string $path): ?string 'afp' => ['application/vnd.ibm.modcap'], 'ag' => ['image/x-applix-graphics'], 'agb' => ['application/x-gba-rom'], + 'age' => ['application/vnd.age'], 'ahead' => ['application/vnd.ahead.space'], 'ai' => ['application/illustrator', 'application/postscript', 'application/vnd.adobe.illustrator'], 'aif' => ['audio/x-aiff'], @@ -1879,11 +1941,20 @@ public function guessMimeType(string $path): ?string 'amr' => ['audio/amr', 'audio/amr-encrypted'], 'amz' => ['audio/x-amzxml'], 'ani' => ['application/x-navi-animation'], - 'anim[1-9j]' => ['video/x-anim'], + 'anim1' => ['video/x-anim'], + 'anim2' => ['video/x-anim'], + 'anim3' => ['video/x-anim'], + 'anim4' => ['video/x-anim'], + 'anim5' => ['video/x-anim'], + 'anim6' => ['video/x-anim'], + 'anim7' => ['video/x-anim'], + 'anim8' => ['video/x-anim'], + 'anim9' => ['video/x-anim'], + 'animj' => ['video/x-anim'], 'anx' => ['application/annodex', 'application/x-annodex'], 'ape' => ['audio/x-ape'], 'apk' => ['application/vnd.android.package-archive'], - 'apng' => ['image/apng'], + 'apng' => ['image/apng', 'image/vnd.mozilla.apng'], 'appcache' => ['text/cache-manifest'], 'appimage' => ['application/vnd.appimage', 'application/x-iso9660-appimage'], 'application' => ['application/x-ms-application'], @@ -1893,6 +1964,7 @@ public function guessMimeType(string $path): ?string 'arj' => ['application/x-arj'], 'arw' => ['image/x-sony-arw'], 'as' => ['application/x-applix-spreadsheet'], + 'asar' => ['application/x-asar'], 'asc' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature', 'text/plain'], 'asd' => ['text/x-common-lisp'], 'asf' => ['application/vnd.ms-asf', 'video/x-ms-asf', 'video/x-ms-asf-plugin', 'video/x-ms-wm'], @@ -1911,8 +1983,10 @@ public function guessMimeType(string $path): ?string 'atx' => ['application/vnd.antix.game-component'], 'au' => ['audio/basic'], 'automount' => ['text/x-systemd-unit'], - 'avf' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], - 'avi' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'avci' => ['image/avci'], + 'avcs' => ['image/avcs'], + 'avf' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.avi', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'avi' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.avi', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], 'avif' => ['image/avif', 'image/avif-sequence'], 'avifs' => ['image/avif', 'image/avif-sequence'], 'aw' => ['application/applixware', 'application/x-applix-word'], @@ -1927,7 +2001,7 @@ public function guessMimeType(string $path): ?string 'azw3' => ['application/vnd.amazon.mobi8-ebook', 'application/x-mobi8-ebook'], 'b16' => ['image/vnd.pco.b16'], 'bak' => ['application/x-trash'], - 'bat' => ['application/x-msdownload'], + 'bat' => ['application/bat', 'application/x-bat', 'application/x-msdownload'], 'bcpio' => ['application/x-bcpio'], 'bdf' => ['application/x-font-bdf'], 'bdm' => ['application/vnd.syncml.dm+wbxml', 'video/mp2t'], @@ -1943,6 +2017,7 @@ public function guessMimeType(string $path): ?string 'blend' => ['application/x-blender'], 'blender' => ['application/x-blender'], 'blorb' => ['application/x-blorb'], + 'blp' => ['text/x-blueprint'], 'bmi' => ['application/vnd.bmi'], 'bmml' => ['application/vnd.balsamiq.bmml+xml'], 'bmp' => ['image/bmp', 'image/x-bmp', 'image/x-ms-bmp'], @@ -1953,8 +2028,9 @@ public function guessMimeType(string $path): ?string 'bsdiff' => ['application/x-bsdiff'], 'bsp' => ['model/vnd.valve.source.compiled-map'], 'btif' => ['image/prs.btif'], - 'bz' => ['application/bzip2', 'application/x-bzip', 'application/x-bzip2'], - 'bz2' => ['application/x-bz2', 'application/bzip2', 'application/x-bzip', 'application/x-bzip2'], + 'bz' => ['application/bzip2', 'application/x-bzip'], + 'bz2' => ['application/x-bz2', 'application/bzip2', 'application/x-bzip2'], + 'bz3' => ['application/x-bzip3'], 'c' => ['text/x-c', 'text/x-csrc'], 'c++' => ['text/x-c++src'], 'c11amc' => ['application/vnd.cluetrust.cartomobile-config'], @@ -1972,6 +2048,7 @@ public function guessMimeType(string $path): ?string 'cb7' => ['application/x-cb7', 'application/x-cbr'], 'cba' => ['application/x-cbr'], 'cbl' => ['text/x-cobol'], + 'cbor' => ['application/cbor'], 'cbr' => ['application/vnd.comicbook-rar', 'application/x-cbr'], 'cbt' => ['application/x-cbr', 'application/x-cbt'], 'cbz' => ['application/vnd.comicbook+zip', 'application/x-cbr', 'application/x-cbz'], @@ -2033,6 +2110,7 @@ public function guessMimeType(string $path): ?string 'cpi' => ['video/mp2t'], 'cpio' => ['application/x-cpio'], 'cpio.gz' => ['application/x-cpio-compressed'], + 'cpl' => ['application/cpl+xml'], 'cpp' => ['text/x-c', 'text/x-c++src'], 'cpt' => ['application/mac-compactpro'], 'cr' => ['text/crystal', 'text/x-crystal'], @@ -2087,6 +2165,7 @@ public function guessMimeType(string $path): ?string 'desktop' => ['application/x-desktop', 'application/x-gnome-app-info'], 'device' => ['text/x-systemd-unit'], 'dfac' => ['application/vnd.dreamfactory'], + 'dff' => ['audio/dff', 'audio/x-dff'], 'dgc' => ['application/x-dgc-compressed'], 'di' => ['text/x-dsrc'], 'dia' => ['application/x-dia-diagram'], @@ -2096,7 +2175,7 @@ public function guessMimeType(string $path): ?string 'dir' => ['application/x-director'], 'dis' => ['application/vnd.mobius.dis'], 'disposition-notification' => ['message/disposition-notification'], - 'divx' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'divx' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.avi', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], 'djv' => ['image/vnd.djvu', 'image/vnd.djvu+multipage', 'image/x-djvu', 'image/x.djvu'], 'djvu' => ['image/vnd.djvu', 'image/vnd.djvu+multipage', 'image/x-djvu', 'image/x.djvu'], 'dll' => ['application/x-msdownload'], @@ -2114,14 +2193,17 @@ public function guessMimeType(string $path): ?string 'dp' => ['application/vnd.osgi.dp'], 'dpg' => ['application/vnd.dpgraph'], 'dra' => ['audio/vnd.dra'], + 'drl' => ['application/x-excellon'], 'drle' => ['image/dicom-rle'], 'dsc' => ['text/prs.lines.tag'], + 'dsf' => ['audio/dsd', 'audio/dsf', 'audio/x-dsd', 'audio/x-dsf'], 'dsl' => ['text/x-dsl'], 'dssc' => ['application/dssc+der'], - 'dtb' => ['application/x-dtbook+xml'], + 'dtb' => ['application/x-dtbook+xml', 'text/x-devicetree-binary'], 'dtd' => ['application/xml-dtd', 'text/x-dtd'], - 'dts' => ['audio/vnd.dts', 'audio/x-dts'], + 'dts' => ['audio/vnd.dts', 'audio/x-dts', 'text/x-devicetree-source'], 'dtshd' => ['audio/vnd.dts.hd', 'audio/x-dtshd'], + 'dtsi' => ['text/x-devicetree-source'], 'dtx' => ['application/x-tex', 'text/x-tex'], 'dv' => ['video/dv'], 'dvb' => ['video/vnd.dvb.file'], @@ -2166,10 +2248,12 @@ public function guessMimeType(string $path): ?string 'epsi.bz2' => ['image/x-bzeps'], 'epsi.gz' => ['image/x-gzeps'], 'epub' => ['application/epub+zip'], + 'eris' => ['application/x-eris-link+cbor'], 'erl' => ['text/x-erlang'], 'es' => ['application/ecmascript', 'text/ecmascript'], 'es3' => ['application/vnd.eszigno3+xml'], 'esa' => ['application/vnd.osgi.subsystem'], + 'escn' => ['application/x-godot-scene'], 'esf' => ['application/vnd.epson.esf'], 'et3' => ['application/vnd.eszigno3+xml'], 'etheme' => ['application/x-e-theme'], @@ -2179,6 +2263,7 @@ public function guessMimeType(string $path): ?string 'ex' => ['text/x-elixir'], 'exe' => ['application/x-ms-dos-executable', 'application/x-msdos-program', 'application/x-msdownload'], 'exi' => ['application/exi'], + 'exp' => ['application/express'], 'exr' => ['image/aces', 'image/x-exr'], 'exs' => ['text/x-elixir'], 'ext' => ['application/vnd.novadigm.ext'], @@ -2212,7 +2297,9 @@ public function guessMimeType(string $path): ?string 'fh7' => ['image/x-freehand'], 'fhc' => ['image/x-freehand'], 'fig' => ['application/x-xfig', 'image/x-xfig'], - 'fits' => ['image/fits', 'image/x-fits'], + 'fish' => ['application/x-fishscript', 'text/x-fish'], + 'fit' => ['application/fits', 'image/fits', 'image/x-fits'], + 'fits' => ['application/fits', 'image/fits', 'image/x-fits'], 'fl' => ['application/x-fluid'], 'flac' => ['audio/flac', 'audio/x-flac'], 'flatpak' => ['application/vnd.flatpak', 'application/vnd.xdgapp'], @@ -2239,6 +2326,7 @@ public function guessMimeType(string $path): ?string 'fst' => ['image/vnd.fst'], 'ftc' => ['application/vnd.fluxtime.clip'], 'fti' => ['application/vnd.anser-web-funds-transfer-initiation'], + 'fts' => ['application/fits', 'image/fits', 'image/x-fits'], 'fvt' => ['video/vnd.fvt'], 'fxm' => ['video/x-javafx'], 'fxp' => ['application/vnd.adobe.fxp'], @@ -2252,15 +2340,18 @@ public function guessMimeType(string $path): ?string 'gb' => ['application/x-gameboy-rom'], 'gba' => ['application/x-gba-rom'], 'gbc' => ['application/x-gameboy-color-rom'], - 'gbr' => ['application/rpki-ghostbusters', 'image/x-gimp-gbr'], + 'gbr' => ['application/rpki-ghostbusters', 'application/vnd.gerber', 'application/x-gerber', 'image/x-gimp-gbr'], + 'gbrjob' => ['application/x-gerber-job'], 'gca' => ['application/x-gca-compressed'], 'gcode' => ['text/x.gcode'], 'gcrd' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'gd' => ['application/x-gdscript'], 'gdi' => ['application/x-gd-rom-cue'], 'gdl' => ['model/vnd.gdl'], 'gdoc' => ['application/vnd.google-apps.document'], - 'ged' => ['application/x-gedcom', 'text/gedcom'], - 'gedcom' => ['application/x-gedcom', 'text/gedcom'], + 'gdshader' => ['application/x-godot-shader'], + 'ged' => ['application/x-gedcom', 'text/gedcom', 'text/vnd.familysearch.gedcom'], + 'gedcom' => ['application/x-gedcom', 'text/gedcom', 'text/vnd.familysearch.gedcom'], 'gem' => ['application/x-gtar', 'application/x-tar'], 'gen' => ['application/x-genesis-rom'], 'geo' => ['application/vnd.dynageo'], @@ -2314,6 +2405,7 @@ public function guessMimeType(string $path): ?string 'gv' => ['text/vnd.graphviz'], 'gvp' => ['text/google-video-pointer', 'text/x-google-video-pointer'], 'gvy' => ['text/x-groovy'], + 'gx' => ['text/x-gcode-gx'], 'gxf' => ['application/gxf'], 'gxt' => ['application/vnd.geonext'], 'gy' => ['text/x-groovy'], @@ -2332,6 +2424,7 @@ public function guessMimeType(string $path): ?string 'hdf' => ['application/x-hdf'], 'hdf4' => ['application/x-hdf'], 'hdf5' => ['application/x-hdf'], + 'hdp' => ['image/jxr', 'image/vnd.ms-photo'], 'heic' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], 'heics' => ['image/heic-sequence'], 'heif' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], @@ -2340,6 +2433,7 @@ public function guessMimeType(string $path): ?string 'held' => ['application/atsc-held+xml'], 'hfe' => ['application/x-hfe-file', 'application/x-hfe-floppy-image'], 'hh' => ['text/x-c', 'text/x-c++hdr'], + 'hif' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], 'hjson' => ['application/hjson'], 'hlp' => ['application/winhlp', 'zz-application/zz-winassoc-hlp'], 'hp' => ['text/x-c++hdr'], @@ -2382,7 +2476,7 @@ public function guessMimeType(string $path): ?string 'iif' => ['application/vnd.shana.informed.interchange'], 'ilbm' => ['image/x-iff', 'image/x-ilbm'], 'ime' => ['audio/imelody', 'audio/x-imelody', 'text/x-imelody'], - 'img' => ['application/x-raw-disk-image'], + 'img' => ['application/vnd.efi.img', 'application/x-raw-disk-image'], 'img.xz' => ['application/x-raw-disk-image-xz-compressed'], 'imp' => ['application/vnd.accpac.simply.imp'], 'ims' => ['application/vnd.ms-ims'], @@ -2401,8 +2495,8 @@ public function guessMimeType(string $path): ?string 'ipynb' => ['application/x-ipynb+json'], 'irm' => ['application/vnd.ibm.rights-management'], 'irp' => ['application/vnd.irepository.package+xml'], - 'iso' => ['application/x-cd-image', 'application/x-dreamcast-rom', 'application/x-gamecube-iso-image', 'application/x-gamecube-rom', 'application/x-iso9660-image', 'application/x-saturn-rom', 'application/x-sega-cd-rom', 'application/x-sega-pico-rom', 'application/x-wbfs', 'application/x-wia', 'application/x-wii-iso-image', 'application/x-wii-rom'], - 'iso9660' => ['application/x-cd-image', 'application/x-iso9660-image'], + 'iso' => ['application/vnd.efi.iso', 'application/x-cd-image', 'application/x-dreamcast-rom', 'application/x-gamecube-iso-image', 'application/x-gamecube-rom', 'application/x-iso9660-image', 'application/x-saturn-rom', 'application/x-sega-cd-rom', 'application/x-sega-pico-rom', 'application/x-wbfs', 'application/x-wia', 'application/x-wii-iso-image', 'application/x-wii-rom'], + 'iso9660' => ['application/vnd.efi.iso', 'application/x-cd-image', 'application/x-iso9660-image'], 'it' => ['audio/x-it'], 'it87' => ['application/x-it87'], 'itp' => ['application/vnd.shana.informed.formtemplate'], @@ -2421,6 +2515,7 @@ public function guessMimeType(string $path): ?string 'jhc' => ['image/jphc'], 'jisp' => ['application/vnd.jisp'], 'jks' => ['application/x-java-keystore'], + 'jl' => ['text/julia'], 'jls' => ['image/jls'], 'jlt' => ['application/vnd.hp-jlyt'], 'jng' => ['image/x-jng'], @@ -2449,7 +2544,7 @@ public function guessMimeType(string $path): ?string 'jsonml' => ['application/jsonml+json'], 'jsx' => ['text/jsx'], 'jxl' => ['image/jxl'], - 'jxr' => ['image/jxr'], + 'jxr' => ['image/jxr', 'image/vnd.ms-photo'], 'jxra' => ['image/jxra'], 'jxrs' => ['image/jxrs'], 'jxs' => ['image/jxs'], @@ -2514,6 +2609,7 @@ public function guessMimeType(string $path): ?string 'list3820' => ['application/vnd.ibm.modcap'], 'listafp' => ['application/vnd.ibm.modcap'], 'litcoffee' => ['text/coffeescript'], + 'lmdb' => ['application/x-lmdb'], 'lnk' => ['application/x-ms-shortcut'], 'lnx' => ['application/x-atari-lynx-rom'], 'loas' => ['audio/usac'], @@ -2581,7 +2677,7 @@ public function guessMimeType(string $path): ?string 'mcd' => ['application/vnd.mcd'], 'mcurl' => ['text/vnd.curl.mcurl'], 'md' => ['text/markdown', 'text/x-markdown'], - 'mdb' => ['application/x-msaccess', 'application/mdb', 'application/msaccess', 'application/vnd.ms-access', 'application/vnd.msaccess', 'application/x-mdb', 'zz-application/zz-winassoc-mdb'], + 'mdb' => ['application/x-msaccess', 'application/mdb', 'application/msaccess', 'application/vnd.ms-access', 'application/vnd.msaccess', 'application/x-lmdb', 'application/x-mdb', 'zz-application/zz-winassoc-mdb'], 'mdi' => ['image/vnd.ms-modi'], 'mdx' => ['application/x-genesis-32x-rom', 'text/mdx'], 'me' => ['text/troff', 'text/x-troff-me'], @@ -2616,7 +2712,7 @@ public function guessMimeType(string $path): ?string 'ml' => ['text/x-ocaml'], 'mli' => ['text/x-ocaml'], 'mlp' => ['application/vnd.dolby.mlp'], - 'mm' => ['text/x-troff-mm'], + 'mm' => ['text/x-objc++src', 'text/x-troff-mm'], 'mmd' => ['application/vnd.chipnuts.karaoke-mmd'], 'mmf' => ['application/vnd.smaf', 'application/x-smaf'], 'mml' => ['application/mathml+xml', 'text/mathml'], @@ -2647,15 +2743,16 @@ public function guessMimeType(string $path): ?string 'mpd' => ['application/dash+xml'], 'mpe' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], 'mpeg' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpf' => ['application/media-policy-dataset+xml'], 'mpg' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], 'mpg4' => ['video/mp4'], 'mpga' => ['audio/mp3', 'audio/mpeg', 'audio/x-mp3', 'audio/x-mpeg', 'audio/x-mpg'], 'mpkg' => ['application/vnd.apple.installer+xml'], - 'mpl' => ['video/mp2t'], + 'mpl' => ['text/x-mpl2', 'video/mp2t'], 'mpls' => ['video/mp2t'], 'mpm' => ['application/vnd.blueice.multipass'], 'mpn' => ['application/vnd.mophun.application'], - 'mpp' => ['application/vnd.ms-project', 'audio/x-musepack'], + 'mpp' => ['application/dash-patch+xml', 'application/vnd.ms-project', 'audio/x-musepack'], 'mpt' => ['application/vnd.ms-project'], 'mpy' => ['application/vnd.ibm.minipay'], 'mqy' => ['application/vnd.mobius.mqy'], @@ -2663,6 +2760,7 @@ public function guessMimeType(string $path): ?string 'mrcx' => ['application/marcxml+xml'], 'mrl' => ['text/x-mrml'], 'mrml' => ['text/x-mrml'], + 'mrpack' => ['application/x-modrinth-modpack+zip'], 'mrw' => ['image/x-minolta-mrw'], 'ms' => ['text/troff', 'text/x-troff-ms'], 'mscml' => ['application/mediaservercontrol+xml'], @@ -2688,7 +2786,7 @@ public function guessMimeType(string $path): ?string 'mwf' => ['application/vnd.mfer'], 'mxf' => ['application/mxf'], 'mxl' => ['application/vnd.recordare.musicxml'], - 'mxmf' => ['audio/mobile-xmf'], + 'mxmf' => ['audio/mobile-xmf', 'audio/vnd.nokia.mobile-xmf'], 'mxml' => ['application/xv+xml'], 'mxs' => ['application/vnd.triscape.mxs'], 'mxu' => ['video/vnd.mpegurl', 'video/x-mpegurl'], @@ -2707,6 +2805,9 @@ public function guessMimeType(string $path): ?string 'ngc' => ['application/x-neo-geo-pocket-color-rom'], 'ngdat' => ['application/vnd.nokia.n-gage.data'], 'ngp' => ['application/x-neo-geo-pocket-rom'], + 'nim' => ['text/x-nim'], + 'nimble' => ['text/x-nimscript'], + 'nims' => ['text/x-nimscript'], 'nitf' => ['application/vnd.nitf'], 'nlu' => ['application/vnd.neurolanguage.nlu'], 'nml' => ['application/vnd.enliven'], @@ -2722,6 +2823,7 @@ public function guessMimeType(string $path): ?string 'nsv' => ['video/x-nsv'], 'nt' => ['application/n-triples'], 'ntf' => ['application/vnd.nitf'], + 'nu' => ['application/x-nuscript', 'text/x-nu'], 'numbers' => ['application/vnd.apple.numbers', 'application/x-iwork-numbers-sffnumbers'], 'nzb' => ['application/x-nzb'], 'o' => ['application/x-object'], @@ -2757,6 +2859,7 @@ public function guessMimeType(string $path): ?string 'onetoc' => ['application/onenote'], 'onetoc2' => ['application/onenote'], 'ooc' => ['text/x-ooc'], + 'openvpn' => ['application/x-openvpn-profile'], 'opf' => ['application/oebps-package+xml'], 'opml' => ['text/x-opml', 'text/x-opml+xml'], 'oprc' => ['application/vnd.palm', 'application/x-palm-database'], @@ -2777,6 +2880,7 @@ public function guessMimeType(string $path): ?string 'ott' => ['application/vnd.oasis.opendocument.text-template'], 'ova' => ['application/ovf', 'application/x-virtualbox-ova'], 'ovf' => ['application/x-virtualbox-ovf'], + 'ovpn' => ['application/x-openvpn-profile'], 'owl' => ['application/rdf+xml', 'text/rdf'], 'owx' => ['application/owl+xml'], 'oxps' => ['application/oxps'], @@ -2816,7 +2920,7 @@ public function guessMimeType(string $path): ?string 'pct' => ['image/x-pict'], 'pcurl' => ['application/vnd.curl.pcurl'], 'pcx' => ['image/vnd.zbrush.pcx', 'image/x-pcx'], - 'pdb' => ['application/vnd.palm', 'application/x-aportisdoc', 'application/x-palm-database', 'application/x-pilot'], + 'pdb' => ['application/vnd.palm', 'application/x-aportisdoc', 'application/x-ms-pdb', 'application/x-palm-database', 'application/x-pilot'], 'pdc' => ['application/x-aportisdoc'], 'pde' => ['text/x-processing'], 'pdf' => ['application/pdf', 'application/acrobat', 'application/nappdf', 'application/x-pdf', 'image/pdf'], @@ -2830,7 +2934,7 @@ public function guessMimeType(string $path): ?string 'pfa' => ['application/x-font-type1'], 'pfb' => ['application/x-font-type1'], 'pfm' => ['application/x-font-type1'], - 'pfr' => ['application/font-tdpfr'], + 'pfr' => ['application/font-tdpfr', 'application/vnd.truedoc'], 'pfx' => ['application/pkcs12', 'application/x-pkcs12'], 'pgm' => ['image/x-portable-graymap'], 'pgn' => ['application/vnd.chess-pgn', 'application/x-chess-pgn'], @@ -2861,7 +2965,7 @@ public function guessMimeType(string $path): ?string 'pm6' => ['application/x-pagemaker'], 'pmd' => ['application/x-pagemaker'], 'pml' => ['application/vnd.ctc-posml'], - 'png' => ['image/png'], + 'png' => ['image/png', 'image/apng', 'image/vnd.mozilla.apng'], 'pnm' => ['image/x-portable-anymap'], 'pntg' => ['image/x-macpaint'], 'po' => ['application/x-gettext', 'text/x-gettext-translation', 'text/x-po'], @@ -2925,8 +3029,10 @@ public function guessMimeType(string $path): ?string 'qml' => ['text/x-qml'], 'qmlproject' => ['text/x-qml'], 'qmltypes' => ['text/x-qml'], + 'qoi' => ['image/qoi'], 'qp' => ['application/x-qpress'], 'qps' => ['application/vnd.publishare-delta-tree'], + 'qs' => ['application/sparql-query'], 'qt' => ['video/quicktime'], 'qti' => ['application/x-qtiplot'], 'qti.gz' => ['application/x-qtiplot'], @@ -2947,7 +3053,7 @@ public function guessMimeType(string $path): ?string 'rar' => ['application/x-rar-compressed', 'application/vnd.rar', 'application/x-rar'], 'ras' => ['image/x-cmu-raster'], 'raw' => ['image/x-panasonic-raw', 'image/x-panasonic-rw'], - 'raw-disk-image' => ['application/x-raw-disk-image'], + 'raw-disk-image' => ['application/vnd.efi.img', 'application/x-raw-disk-image'], 'raw-disk-image.xz' => ['application/x-raw-disk-image-xz-compressed'], 'rax' => ['audio/vnd.m-realaudio', 'audio/vnd.rn-realaudio', 'audio/x-pn-realaudio'], 'rb' => ['application/x-ruby'], @@ -2959,7 +3065,7 @@ public function guessMimeType(string $path): ?string 'rej' => ['application/x-reject', 'text/x-reject'], 'relo' => ['application/p2p-overlay+xml'], 'rep' => ['application/vnd.businessobjects'], - 'res' => ['application/x-dtbresource+xml'], + 'res' => ['application/x-dtbresource+xml', 'application/x-godot-resource'], 'rgb' => ['image/x-rgb'], 'rif' => ['application/reginfo+xml'], 'rip' => ['audio/vnd.rip'], @@ -3015,6 +3121,7 @@ public function guessMimeType(string $path): ?string 'scala' => ['text/x-scala'], 'scd' => ['application/x-msschedule'], 'scm' => ['application/vnd.lotus-screencam', 'text/x-scheme'], + 'scn' => ['application/x-godot-scene'], 'scope' => ['text/x-systemd-unit'], 'scq' => ['application/scvp-cv-request'], 'scs' => ['application/scvp-cv-response'], @@ -3069,7 +3176,7 @@ public function guessMimeType(string $path): ?string 'sis' => ['application/vnd.symbian.install'], 'sisx' => ['application/vnd.symbian.install', 'x-epoc/x-sisx-app'], 'sit' => ['application/x-stuffit', 'application/stuffit', 'application/x-sit'], - 'sitx' => ['application/x-stuffitx'], + 'sitx' => ['application/x-sitx', 'application/x-stuffitx'], 'siv' => ['application/sieve'], 'sk' => ['image/x-skencil'], 'sk1' => ['image/x-skencil'], @@ -3139,6 +3246,7 @@ public function guessMimeType(string $path): ?string 'stk' => ['application/hyperstudio'], 'stl' => ['application/vnd.ms-pki.stl', 'model/stl', 'model/x.stl-ascii', 'model/x.stl-binary'], 'stm' => ['audio/x-stm'], + 'stpx' => ['model/step+xml'], 'stpxz' => ['model/step-xml+zip'], 'stpz' => ['model/step+zip'], 'str' => ['application/vnd.pg.format'], @@ -3156,6 +3264,7 @@ public function guessMimeType(string $path): ?string 'svc' => ['application/vnd.dvb.service'], 'svd' => ['application/vnd.svd'], 'svg' => ['image/svg+xml', 'image/svg'], + 'svg.gz' => ['image/svg+xml-compressed'], 'svgz' => ['image/svg+xml', 'image/svg+xml-compressed'], 'svh' => ['text/x-svhdr'], 'swa' => ['application/x-director'], @@ -3176,12 +3285,14 @@ public function guessMimeType(string $path): ?string 't3' => ['application/x-t3vm-image'], 't38' => ['image/t38'], 'taglet' => ['application/vnd.mynfc'], + 'tak' => ['audio/x-tak'], 'tao' => ['application/vnd.tao.intent-module-archive'], 'tap' => ['image/vnd.tencent.tap'], 'tar' => ['application/x-tar', 'application/x-gtar'], 'tar.Z' => ['application/x-tarz'], 'tar.bz' => ['application/x-bzip-compressed-tar'], - 'tar.bz2' => ['application/x-bzip-compressed-tar'], + 'tar.bz2' => ['application/x-bzip2-compressed-tar'], + 'tar.bz3' => ['application/x-bzip3-compressed-tar'], 'tar.gz' => ['application/x-compressed-tar'], 'tar.lrz' => ['application/x-lrzip-compressed-tar'], 'tar.lz' => ['application/x-lzip-compressed-tar'], @@ -3192,9 +3303,10 @@ public function guessMimeType(string $path): ?string 'tar.zst' => ['application/x-zstd-compressed-tar'], 'target' => ['text/x-systemd-unit'], 'taz' => ['application/x-tarz'], - 'tb2' => ['application/x-bzip-compressed-tar'], + 'tb2' => ['application/x-bzip2-compressed-tar', 'application/x-bzip-compressed-tar'], 'tbz' => ['application/x-bzip-compressed-tar'], - 'tbz2' => ['application/x-bzip-compressed-tar'], + 'tbz2' => ['application/x-bzip2-compressed-tar', 'application/x-bzip-compressed-tar'], + 'tbz3' => ['application/x-bzip3-compressed-tar'], 'tcap' => ['application/vnd.3gpp2.tcap'], 'tcl' => ['application/x-tcl', 'text/tcl', 'text/x-tcl'], 'td' => ['application/urc-targetdesc+xml'], @@ -3220,6 +3332,7 @@ public function guessMimeType(string $path): ?string 'tlrz' => ['application/x-lrzip-compressed-tar'], 'tlz' => ['application/x-lzma-compressed-tar'], 'tmo' => ['application/vnd.tmobile-livetv'], + 'tmx' => ['application/x-tiled-tmx'], 'tnef' => ['application/ms-tnef', 'application/vnd.ms-tnef'], 'tnf' => ['application/ms-tnef', 'application/vnd.ms-tnef'], 'toc' => ['application/x-cdrdao-toc'], @@ -3230,11 +3343,14 @@ public function guessMimeType(string $path): ?string 'tpt' => ['application/vnd.trid.tpt'], 'tr' => ['application/x-troff', 'text/troff', 'text/x-troff'], 'tra' => ['application/vnd.trueapp'], + 'tres' => ['application/x-godot-resource'], 'trig' => ['application/trig', 'application/x-trig'], 'trm' => ['application/x-msterminal'], 'ts' => ['application/x-linguist', 'text/vnd.qt.linguist', 'text/vnd.trolltech.linguist', 'video/mp2t'], + 'tscn' => ['application/x-godot-scene'], 'tsd' => ['application/timestamped-data'], 'tsv' => ['text/tab-separated-values'], + 'tsx' => ['application/x-tiled-tsx'], 'tta' => ['audio/tta', 'audio/x-tta'], 'ttc' => ['font/collection'], 'ttf' => ['application/x-font-truetype', 'application/x-font-ttf', 'font/ttf'], @@ -3248,6 +3364,7 @@ public function guessMimeType(string $path): ?string 'txf' => ['application/vnd.mobius.txf'], 'txt' => ['text/plain'], 'txz' => ['application/x-xz-compressed-tar'], + 'typ' => ['text/x-typst'], 'tzo' => ['application/x-tzo'], 'tzst' => ['application/x-zstd-compressed-tar'], 'u32' => ['application/x-authorware-bin'], @@ -3370,7 +3487,7 @@ public function guessMimeType(string $path): ?string 'wbxml' => ['application/vnd.wap.wbxml'], 'wcm' => ['application/vnd.ms-works'], 'wdb' => ['application/vnd.ms-works'], - 'wdp' => ['image/vnd.ms-photo'], + 'wdp' => ['image/jxr', 'image/vnd.ms-photo'], 'weba' => ['audio/webm'], 'webapp' => ['application/x-web-app-manifest+json'], 'webm' => ['video/webm'], @@ -3378,6 +3495,7 @@ public function guessMimeType(string $path): ?string 'webp' => ['image/webp'], 'wg' => ['application/vnd.pmi.widget'], 'wgt' => ['application/widget'], + 'wif' => ['application/watcherinfo+xml'], 'wim' => ['application/x-ms-wim'], 'wk1' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], 'wk3' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], @@ -3480,7 +3598,7 @@ public function guessMimeType(string $path): ?string 'xltx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.template'], 'xlw' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], 'xm' => ['audio/x-xm', 'audio/xm'], - 'xmf' => ['audio/mobile-xmf', 'audio/x-xmf', 'audio/xmf'], + 'xmf' => ['audio/x-xmf', 'audio/xmf'], 'xmi' => ['text/x-xmi'], 'xml' => ['application/xml', 'text/xml'], 'xns' => ['application/xcap-ns+xml'], @@ -3505,12 +3623,12 @@ public function guessMimeType(string $path): ?string 'xwd' => ['image/x-xwindowdump'], 'xyz' => ['chemical/x-xyz'], 'xz' => ['application/x-xz'], - 'yaml' => ['application/x-yaml', 'text/x-yaml', 'text/yaml'], + 'yaml' => ['application/yaml', 'application/x-yaml', 'text/x-yaml', 'text/yaml'], 'yang' => ['application/yang'], 'yin' => ['application/yin+xml'], - 'yml' => ['application/x-yaml', 'text/x-yaml', 'text/yaml'], + 'yml' => ['application/yaml', 'application/x-yaml', 'text/x-yaml', 'text/yaml'], 'ymp' => ['text/x-suse-ymp'], - 'yt' => ['application/vnd.youtube.yt'], + 'yt' => ['application/vnd.youtube.yt', 'video/vnd.youtube.yt'], 'z1' => ['application/x-zmachine'], 'z2' => ['application/x-zmachine'], 'z3' => ['application/x-zmachine'], @@ -3522,16 +3640,16 @@ public function guessMimeType(string $path): ?string 'z8' => ['application/x-zmachine'], 'zabw' => ['application/x-abiword'], 'zaz' => ['application/vnd.zzazz.deck+xml'], + 'zim' => ['application/x-openzim'], 'zip' => ['application/zip', 'application/x-zip', 'application/x-zip-compressed'], + 'zipx' => ['application/x-zip', 'application/x-zip-compressed', 'application/zip'], 'zir' => ['application/vnd.zul'], 'zirz' => ['application/vnd.zul'], 'zmm' => ['application/vnd.handheld-entertainment+xml'], 'zoo' => ['application/x-zoo'], + 'zpaq' => ['application/x-zpaq'], 'zsav' => ['application/x-spss-sav', 'application/x-spss-savefile'], 'zst' => ['application/zstd'], 'zz' => ['application/zlib'], - '123' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], - '602' => ['application/x-t602'], - '669' => ['audio/x-mod'], ]; } diff --git a/symfony/mime/Part/AbstractMultipartPart.php b/symfony/mime/Part/AbstractMultipartPart.php index 685d25062..1da0ddf33 100644 --- a/symfony/mime/Part/AbstractMultipartPart.php +++ b/symfony/mime/Part/AbstractMultipartPart.php @@ -18,8 +18,8 @@ */ abstract class AbstractMultipartPart extends AbstractPart { - private $boundary; - private $parts = []; + private ?string $boundary = null; + private array $parts = []; public function __construct(AbstractPart ...$parts) { @@ -90,10 +90,6 @@ public function asDebugString(): string private function getBoundary(): string { - if (null === $this->boundary) { - $this->boundary = strtr(base64_encode(random_bytes(6)), '+/', '-_'); - } - - return $this->boundary; + return $this->boundary ??= strtr(base64_encode(random_bytes(6)), '+/', '-_'); } } diff --git a/symfony/mime/Part/AbstractPart.php b/symfony/mime/Part/AbstractPart.php index 93892d9df..130106d68 100644 --- a/symfony/mime/Part/AbstractPart.php +++ b/symfony/mime/Part/AbstractPart.php @@ -18,7 +18,7 @@ */ abstract class AbstractPart { - private $headers; + private Headers $headers; public function __construct() { diff --git a/symfony/mime/Part/DataPart.php b/symfony/mime/Part/DataPart.php index 4247ce798..9d2f3be4b 100644 --- a/symfony/mime/Part/DataPart.php +++ b/symfony/mime/Part/DataPart.php @@ -13,7 +13,6 @@ use Symfony\Component\Mime\Exception\InvalidArgumentException; use Symfony\Component\Mime\Header\Headers; -use Symfony\Component\Mime\MimeTypes; /** * @author Fabien Potencier @@ -21,25 +20,22 @@ class DataPart extends TextPart { /** @internal */ - protected $_parent; + protected array $_parent; - private static $mimeTypes; - - private $filename; - private $mediaType; - private $cid; - private $handle; + private ?string $filename = null; + private string $mediaType; + private ?string $cid = null; /** - * @param resource|string $body + * @param resource|string|File $body Use a File instance to defer loading the file until rendering */ - public function __construct($body, string $filename = null, string $contentType = null, string $encoding = null) + public function __construct($body, ?string $filename = null, ?string $contentType = null, ?string $encoding = null) { - unset($this->_parent); - - if (null === $contentType) { - $contentType = 'application/octet-stream'; + if ($body instanceof File && !$filename) { + $filename = $body->getFilename(); } + + $contentType ??= $body instanceof File ? $body->getContentType() : 'application/octet-stream'; [$this->mediaType, $subtype] = explode('/', $contentType); parent::__construct($body, null, $subtype, $encoding); @@ -51,44 +47,33 @@ public function __construct($body, string $filename = null, string $contentType $this->setDisposition('attachment'); } - public static function fromPath(string $path, string $name = null, string $contentType = null): self + public static function fromPath(string $path, ?string $name = null, ?string $contentType = null): self { - if (null === $contentType) { - $ext = strtolower(substr($path, strrpos($path, '.') + 1)); - if (null === self::$mimeTypes) { - self::$mimeTypes = new MimeTypes(); - } - $contentType = self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream'; - } - - if ((is_file($path) && !is_readable($path)) || is_dir($path)) { - throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path)); - } - - if (false === $handle = @fopen($path, 'r', false)) { - throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path)); - } - - if (!is_file($path)) { - $cache = fopen('php://temp', 'r+'); - stream_copy_to_stream($handle, $cache); - $handle = $cache; - } - - $p = new self($handle, $name ?: basename($path), $contentType); - $p->handle = $handle; - - return $p; + return new self(new File($path), $name, $contentType); } /** * @return $this */ - public function asInline() + public function asInline(): static { return $this->setDisposition('inline'); } + /** + * @return $this + */ + public function setContentId(string $cid): static + { + if (!str_contains($cid, '@')) { + throw new InvalidArgumentException(sprintf('Invalid cid "%s".', $cid)); + } + + $this->cid = $cid; + + return $this; + } + public function getContentId(): string { return $this->cid ?: $this->cid = $this->generateContentId(); @@ -129,22 +114,22 @@ public function asDebugString(): string return $str; } - private function generateContentId(): string + public function getFilename(): ?string { - return bin2hex(random_bytes(16)).'@symfony'; + return $this->filename; } - public function __destruct() + public function getContentType(): string { - if (null !== $this->handle && \is_resource($this->handle)) { - fclose($this->handle); - } + return implode('/', [$this->getMediaType(), $this->getMediaSubtype()]); } - /** - * @return array - */ - public function __sleep() + private function generateContentId(): string + { + return bin2hex(random_bytes(16)).'@symfony'; + } + + public function __sleep(): array { // converts the body to a string parent::__sleep(); @@ -152,7 +137,6 @@ public function __sleep() $this->_parent = []; foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) { $r = new \ReflectionProperty(TextPart::class, $name); - $r->setAccessible(true); $this->_parent[$name] = $r->getValue($this); } $this->_headers = $this->getHeaders(); @@ -160,10 +144,12 @@ public function __sleep() return ['_headers', '_parent', 'filename', 'mediaType']; } + /** + * @return void + */ public function __wakeup() { $r = new \ReflectionProperty(AbstractPart::class, 'headers'); - $r->setAccessible(true); $r->setValue($this, $this->_headers); unset($this->_headers); @@ -171,11 +157,10 @@ public function __wakeup() throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) { - if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name])) { + if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name]) && !$this->_parent[$name] instanceof File) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } $r = new \ReflectionProperty(TextPart::class, $name); - $r->setAccessible(true); $r->setValue($this, $this->_parent[$name]); } unset($this->_parent); diff --git a/symfony/mime/Part/File.php b/symfony/mime/Part/File.php new file mode 100644 index 000000000..cd05a3dee --- /dev/null +++ b/symfony/mime/Part/File.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\MimeTypes; + +/** + * @author Fabien Potencier + */ +class File +{ + private static MimeTypes $mimeTypes; + + public function __construct( + private string $path, + private ?string $filename = null, + ) { + } + + public function getPath(): string + { + return $this->path; + } + + public function getContentType(): string + { + $ext = strtolower(pathinfo($this->path, \PATHINFO_EXTENSION)); + self::$mimeTypes ??= new MimeTypes(); + + return self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream'; + } + + public function getSize(): int + { + return filesize($this->path); + } + + public function getFilename(): string + { + return $this->filename ??= basename($this->getPath()); + } +} diff --git a/symfony/mime/Part/MessagePart.php b/symfony/mime/Part/MessagePart.php index 00129b475..9d30544ae 100644 --- a/symfony/mime/Part/MessagePart.php +++ b/symfony/mime/Part/MessagePart.php @@ -21,7 +21,7 @@ */ class MessagePart extends DataPart { - private $message; + private RawMessage $message; public function __construct(RawMessage $message) { @@ -60,15 +60,12 @@ public function bodyToIterable(): iterable return $this->message->toIterable(); } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { return ['message']; } - public function __wakeup() + public function __wakeup(): void { $this->__construct($this->message); } diff --git a/symfony/mime/Part/Multipart/FormDataPart.php b/symfony/mime/Part/Multipart/FormDataPart.php index ff6df818f..904c86d6b 100644 --- a/symfony/mime/Part/Multipart/FormDataPart.php +++ b/symfony/mime/Part/Multipart/FormDataPart.php @@ -23,7 +23,7 @@ */ final class FormDataPart extends AbstractMultipartPart { - private $fields = []; + private array $fields = []; /** * @param array $fields @@ -32,13 +32,8 @@ public function __construct(array $fields = []) { parent::__construct(); - foreach ($fields as $name => $value) { - if (!\is_string($value) && !\is_array($value) && !$value instanceof TextPart) { - throw new InvalidArgumentException(sprintf('A form field value can only be a string, an array, or an instance of TextPart ("%s" given).', get_debug_type($value))); - } + $this->fields = $fields; - $this->fields[$name] = $value; - } // HTTP does not support \r\n in header values $this->getHeaders()->setMaxLineLength(\PHP_INT_MAX); } @@ -58,7 +53,7 @@ private function prepareFields(array $fields): array $values = []; $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) { - if (\is_int($key) && \is_array($item)) { + if (null === $root && \is_int($key) && \is_array($item)) { if (1 !== \count($item)) { throw new InvalidArgumentException(sprintf('Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, %d provided.', \count($item))); } @@ -75,6 +70,10 @@ private function prepareFields(array $fields): array return; } + if (!\is_string($item) && !$item instanceof TextPart) { + throw new InvalidArgumentException(sprintf('The value of the form field "%s" can only be a string, an array, or an instance of TextPart, "%s" given.', $fieldName, get_debug_type($item))); + } + $values[] = $this->preparePart($fieldName, $item); }; @@ -83,7 +82,7 @@ private function prepareFields(array $fields): array return $values; } - private function preparePart(string $name, $value): TextPart + private function preparePart(string $name, string|TextPart $value): TextPart { if (\is_string($value)) { return $this->configurePart($name, new TextPart($value, 'utf-8', 'plain', '8bit')); @@ -96,10 +95,7 @@ private function configurePart(string $name, TextPart $part): TextPart { static $r; - if (null === $r) { - $r = new \ReflectionProperty(TextPart::class, 'encoding'); - $r->setAccessible(true); - } + $r ??= new \ReflectionProperty(TextPart::class, 'encoding'); $part->setDisposition('form-data'); $part->setName($name); diff --git a/symfony/mime/Part/Multipart/RelatedPart.php b/symfony/mime/Part/Multipart/RelatedPart.php index 08fdd5fa9..a0d6a1c2c 100644 --- a/symfony/mime/Part/Multipart/RelatedPart.php +++ b/symfony/mime/Part/Multipart/RelatedPart.php @@ -19,7 +19,7 @@ */ final class RelatedPart extends AbstractMultipartPart { - private $mainPart; + private AbstractPart $mainPart; public function __construct(AbstractPart $mainPart, AbstractPart $part, AbstractPart ...$parts) { diff --git a/symfony/mime/Part/SMimePart.php b/symfony/mime/Part/SMimePart.php index cb619c293..57b9766b1 100644 --- a/symfony/mime/Part/SMimePart.php +++ b/symfony/mime/Part/SMimePart.php @@ -19,26 +19,17 @@ class SMimePart extends AbstractPart { /** @internal */ - protected $_headers; + protected Headers $_headers; - private $body; - private $type; - private $subtype; - private $parameters; + private iterable|string $body; + private string $type; + private string $subtype; + private array $parameters; - /** - * @param iterable|string $body - */ - public function __construct($body, string $type, string $subtype, array $parameters) + public function __construct(iterable|string $body, string $type, string $subtype, array $parameters) { - unset($this->_headers); - parent::__construct(); - if (!\is_string($body) && !is_iterable($body)) { - throw new \TypeError(sprintf('The body of "%s" must be a string or a iterable (got "%s").', self::class, get_debug_type($body))); - } - $this->body = $body; $this->type = $type; $this->subtype = $subtype; @@ -114,7 +105,6 @@ public function __sleep(): array public function __wakeup(): void { $r = new \ReflectionProperty(AbstractPart::class, 'headers'); - $r->setAccessible(true); $r->setValue($this, $this->_headers); unset($this->_headers); } diff --git a/symfony/mime/Part/TextPart.php b/symfony/mime/Part/TextPart.php index bfe41c0aa..2a8dd5852 100644 --- a/symfony/mime/Part/TextPart.php +++ b/symfony/mime/Part/TextPart.php @@ -24,32 +24,35 @@ class TextPart extends AbstractPart { /** @internal */ - protected $_headers; + protected Headers $_headers; - private static $encoders = []; + private static array $encoders = []; + /** @var resource|string|File */ private $body; - private $charset; - private $subtype; - /** - * @var ?string - */ - private $disposition; - private $name; - private $encoding; - private $seekable; + private ?string $charset; + private string $subtype; + private ?string $disposition = null; + private ?string $name = null; + private string $encoding; + private ?bool $seekable = null; /** - * @param resource|string $body + * @param resource|string|File $body Use a File instance to defer loading the file until rendering */ - public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', string $encoding = null) + public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', ?string $encoding = null) { - unset($this->_headers); - parent::__construct(); - if (!\is_string($body) && !\is_resource($body)) { - throw new \TypeError(sprintf('The body of "%s" must be a string or a resource (got "%s").', self::class, get_debug_type($body))); + if (!\is_string($body) && !\is_resource($body) && !$body instanceof File) { + throw new \TypeError(sprintf('The body of "%s" must be a string, a resource, or an instance of "%s" (got "%s").', self::class, File::class, get_debug_type($body))); + } + + if ($body instanceof File) { + $path = $body->getPath(); + if ((is_file($path) && !is_readable($path)) || is_dir($path)) { + throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path)); + } } $this->body = $body; @@ -82,27 +85,51 @@ public function getMediaSubtype(): string * * @return $this */ - public function setDisposition(string $disposition) + public function setDisposition(string $disposition): static { $this->disposition = $disposition; return $this; } + /** + * @return ?string null or one of attachment, inline, or form-data + */ + public function getDisposition(): ?string + { + return $this->disposition; + } + /** * Sets the name of the file (used by FormDataPart). * * @return $this */ - public function setName(string $name) + public function setName(string $name): static { $this->name = $name; return $this; } + /** + * Gets the name of the file. + */ + public function getName(): ?string + { + return $this->name; + } + public function getBody(): string { + if ($this->body instanceof File) { + if (false === $ret = @file_get_contents($this->body->getPath())) { + throw new InvalidArgumentException(error_get_last()['message']); + } + + return $ret; + } + if (null === $this->seekable) { return $this->body; } @@ -121,7 +148,14 @@ public function bodyToString(): string public function bodyToIterable(): iterable { - if (null !== $this->seekable) { + if ($this->body instanceof File) { + $path = $this->body->getPath(); + if (false === $handle = @fopen($path, 'r', false)) { + throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path)); + } + + yield from $this->getEncoder()->encodeByteStream($handle); + } elseif (null !== $this->seekable) { if ($this->seekable) { rewind($this->body); } @@ -170,14 +204,14 @@ public function asDebugString(): string private function getEncoder(): ContentEncoderInterface { if ('8bit' === $this->encoding) { - return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new EightBitContentEncoder()); + return self::$encoders[$this->encoding] ??= new EightBitContentEncoder(); } if ('quoted-printable' === $this->encoding) { - return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new QpContentEncoder()); + return self::$encoders[$this->encoding] ??= new QpContentEncoder(); } - return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new Base64ContentEncoder()); + return self::$encoders[$this->encoding] ??= new Base64ContentEncoder(); } private function chooseEncoding(): string @@ -189,10 +223,7 @@ private function chooseEncoding(): string return 'quoted-printable'; } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { // convert resources to strings for serialization if (null !== $this->seekable) { @@ -205,10 +236,12 @@ public function __sleep() return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding']; } + /** + * @return void + */ public function __wakeup() { $r = new \ReflectionProperty(AbstractPart::class, 'headers'); - $r->setAccessible(true); $r->setValue($this, $this->_headers); unset($this->_headers); } diff --git a/symfony/mime/RawMessage.php b/symfony/mime/RawMessage.php index d2a311dae..2b1b52cde 100644 --- a/symfony/mime/RawMessage.php +++ b/symfony/mime/RawMessage.php @@ -16,69 +16,93 @@ /** * @author Fabien Potencier */ -class RawMessage implements \Serializable +class RawMessage { + /** @var iterable|string|resource */ private $message; + private bool $isGeneratorClosed; /** - * @param iterable|string $message + * @param iterable|string|resource $message */ - public function __construct($message) + public function __construct(mixed $message) { $this->message = $message; } + public function __destruct() + { + if (\is_resource($this->message)) { + fclose($this->message); + } + } + public function toString(): string { if (\is_string($this->message)) { return $this->message; } - if ($this->message instanceof \Traversable) { - $this->message = iterator_to_array($this->message, false); + + if (\is_resource($this->message)) { + return stream_get_contents($this->message, -1, 0); } - return $this->message = implode('', $this->message); + $message = ''; + foreach ($this->message as $chunk) { + $message .= $chunk; + } + + return $this->message = $message; } public function toIterable(): iterable { + if ($this->isGeneratorClosed ?? false) { + trigger_deprecation('symfony/mime', '6.4', 'Sending an email with a closed generator is deprecated and will throw in 7.0.'); + // throw new LogicException('Unable to send the email as its generator is already closed.'); + } + if (\is_string($this->message)) { yield $this->message; return; } - $message = ''; + if (\is_resource($this->message)) { + rewind($this->message); + while ($line = fgets($this->message)) { + yield $line; + } + + return; + } + + if ($this->message instanceof \Generator) { + $message = fopen('php://temp', 'w+'); + foreach ($this->message as $chunk) { + fwrite($message, $chunk); + yield $chunk; + } + $this->isGeneratorClosed = !$this->message->valid(); + $this->message = $message; + + return; + } + foreach ($this->message as $chunk) { - $message .= $chunk; yield $chunk; } - $this->message = $message; } /** + * @return void + * * @throws LogicException if the message is not valid */ public function ensureValidity() { } - /** - * @internal - */ - final public function serialize(): string - { - return serialize($this->__serialize()); - } - - /** - * @internal - */ - final public function unserialize($serialized) - { - $this->__unserialize(unserialize($serialized)); - } - public function __serialize(): array { return [$this->toString()]; diff --git a/symfony/polyfill-intl-idn/Idn.php b/symfony/polyfill-intl-idn/Idn.php index 3dc061209..334f8ee70 100644 --- a/symfony/polyfill-intl-idn/Idn.php +++ b/symfony/polyfill-intl-idn/Idn.php @@ -145,7 +145,7 @@ final class Idn */ public static function idn_to_ascii($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) { - if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + if (self::INTL_IDNA_VARIANT_2003 === $variant) { @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); } @@ -198,7 +198,7 @@ public static function idn_to_ascii($domainName, $options = self::IDNA_DEFAULT, */ public static function idn_to_utf8($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) { - if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + if (self::INTL_IDNA_VARIANT_2003 === $variant) { @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); } @@ -280,10 +280,6 @@ private static function mapCodePoints($input, array $options, Info $info) switch ($data['status']) { case 'disallowed': - $info->errors |= self::ERROR_DISALLOWED; - - // no break. - case 'valid': $str .= mb_chr($codePoint, 'utf-8'); @@ -294,7 +290,7 @@ private static function mapCodePoints($input, array $options, Info $info) break; case 'mapped': - $str .= $data['mapping']; + $str .= $transitional && 0x1E9E === $codePoint ? 'ss' : $data['mapping']; break; @@ -346,6 +342,18 @@ private static function process($domain, array $options, Info $info) $validationOptions = $options; if ('xn--' === substr($label, 0, 4)) { + // Step 4.1. If the label contains any non-ASCII code point (i.e., a code point greater than U+007F), + // record that there was an error, and continue with the next label. + if (preg_match('/[^\x00-\x7F]/', $label)) { + $info->errors |= self::ERROR_PUNYCODE; + + continue; + } + + // Step 4.2. Attempt to convert the rest of the label to Unicode according to Punycode [RFC3492]. If + // that conversion fails, record that there was an error, and continue + // with the next label. Otherwise replace the original label in the string by the results of the + // conversion. try { $label = self::punycodeDecode(substr($label, 4)); } catch (\Exception $e) { @@ -516,6 +524,8 @@ private static function validateLabel($label, Info $info, array $options, $canBe if ('-' === substr($label, -1, 1)) { $info->errors |= self::ERROR_TRAILING_HYPHEN; } + } elseif ('xn--' === substr($label, 0, 4)) { + $info->errors |= self::ERROR_PUNYCODE; } // Step 4. The label must not contain a U+002E (.) FULL STOP. diff --git a/symfony/polyfill-php72/LICENSE b/symfony/polyfill-php72/LICENSE deleted file mode 100644 index 6e3afce69..000000000 --- a/symfony/polyfill-php72/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2015-present Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/symfony/polyfill-php72/Php72.php b/symfony/polyfill-php72/Php72.php deleted file mode 100644 index 7bf96c996..000000000 --- a/symfony/polyfill-php72/Php72.php +++ /dev/null @@ -1,217 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Polyfill\Php72; - -/** - * @author Nicolas Grekas - * @author Dariusz Rumiński - * - * @internal - */ -final class Php72 -{ - private static $hashMask; - - public static function utf8_encode($s) - { - $s .= $s; - $len = \strlen($s); - - for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) { - switch (true) { - case $s[$i] < "\x80": $s[$j] = $s[$i]; break; - case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break; - default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break; - } - } - - return substr($s, 0, $j); - } - - public static function utf8_decode($s) - { - $s = (string) $s; - $len = \strlen($s); - - for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) { - switch ($s[$i] & "\xF0") { - case "\xC0": - case "\xD0": - $c = (\ord($s[$i] & "\x1F") << 6) | \ord($s[++$i] & "\x3F"); - $s[$j] = $c < 256 ? \chr($c) : '?'; - break; - - case "\xF0": - ++$i; - // no break - - case "\xE0": - $s[$j] = '?'; - $i += 2; - break; - - default: - $s[$j] = $s[$i]; - } - } - - return substr($s, 0, $j); - } - - public static function php_os_family() - { - if ('\\' === \DIRECTORY_SEPARATOR) { - return 'Windows'; - } - - $map = [ - 'Darwin' => 'Darwin', - 'DragonFly' => 'BSD', - 'FreeBSD' => 'BSD', - 'NetBSD' => 'BSD', - 'OpenBSD' => 'BSD', - 'Linux' => 'Linux', - 'SunOS' => 'Solaris', - ]; - - return $map[\PHP_OS] ?? 'Unknown'; - } - - public static function spl_object_id($object) - { - if (null === self::$hashMask) { - self::initHashMask(); - } - if (null === $hash = spl_object_hash($object)) { - return; - } - - // On 32-bit systems, PHP_INT_SIZE is 4, - return self::$hashMask ^ hexdec(substr($hash, 16 - (\PHP_INT_SIZE * 2 - 1), \PHP_INT_SIZE * 2 - 1)); - } - - public static function sapi_windows_vt100_support($stream, $enable = null) - { - if (!\is_resource($stream)) { - trigger_error('sapi_windows_vt100_support() expects parameter 1 to be resource, '.\gettype($stream).' given', \E_USER_WARNING); - - return false; - } - - $meta = stream_get_meta_data($stream); - - if ('STDIO' !== $meta['stream_type']) { - trigger_error('sapi_windows_vt100_support() was not able to analyze the specified stream', \E_USER_WARNING); - - return false; - } - - // We cannot actually disable vt100 support if it is set - if (false === $enable || !self::stream_isatty($stream)) { - return false; - } - - // The native function does not apply to stdin - $meta = array_map('strtolower', $meta); - $stdin = 'php://stdin' === $meta['uri'] || 'php://fd/0' === $meta['uri']; - - return !$stdin - && (false !== getenv('ANSICON') - || 'ON' === getenv('ConEmuANSI') - || 'xterm' === getenv('TERM') - || 'Hyper' === getenv('TERM_PROGRAM')); - } - - public static function stream_isatty($stream) - { - if (!\is_resource($stream)) { - trigger_error('stream_isatty() expects parameter 1 to be resource, '.\gettype($stream).' given', \E_USER_WARNING); - - return false; - } - - if ('\\' === \DIRECTORY_SEPARATOR) { - $stat = @fstat($stream); - // Check if formatted mode is S_IFCHR - return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; - } - - return \function_exists('posix_isatty') && @posix_isatty($stream); - } - - private static function initHashMask() - { - $obj = (object) []; - self::$hashMask = -1; - - // check if we are nested in an output buffering handler to prevent a fatal error with ob_start() below - $obFuncs = ['ob_clean', 'ob_end_clean', 'ob_flush', 'ob_end_flush', 'ob_get_contents', 'ob_get_flush']; - foreach (debug_backtrace(\PHP_VERSION_ID >= 50400 ? \DEBUG_BACKTRACE_IGNORE_ARGS : false) as $frame) { - if (isset($frame['function'][0]) && !isset($frame['class']) && 'o' === $frame['function'][0] && \in_array($frame['function'], $obFuncs)) { - $frame['line'] = 0; - break; - } - } - if (!empty($frame['line'])) { - ob_start(); - debug_zval_dump($obj); - self::$hashMask = (int) substr(ob_get_clean(), 17); - } - - self::$hashMask ^= hexdec(substr(spl_object_hash($obj), 16 - (\PHP_INT_SIZE * 2 - 1), \PHP_INT_SIZE * 2 - 1)); - } - - public static function mb_chr($code, $encoding = null) - { - if (0x80 > $code %= 0x200000) { - $s = \chr($code); - } elseif (0x800 > $code) { - $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); - } elseif (0x10000 > $code) { - $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); - } else { - $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); - } - - if ('UTF-8' !== $encoding = $encoding ?? mb_internal_encoding()) { - $s = mb_convert_encoding($s, $encoding, 'UTF-8'); - } - - return $s; - } - - public static function mb_ord($s, $encoding = null) - { - if (null === $encoding) { - $s = mb_convert_encoding($s, 'UTF-8'); - } elseif ('UTF-8' !== $encoding) { - $s = mb_convert_encoding($s, 'UTF-8', $encoding); - } - - if (1 === \strlen($s)) { - return \ord($s); - } - - $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; - if (0xF0 <= $code) { - return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; - } - if (0xE0 <= $code) { - return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; - } - if (0xC0 <= $code) { - return (($code - 0xC0) << 6) + $s[2] - 0x80; - } - - return $code; - } -} diff --git a/symfony/polyfill-php72/bootstrap.php b/symfony/polyfill-php72/bootstrap.php deleted file mode 100644 index b5c92d4c7..000000000 --- a/symfony/polyfill-php72/bootstrap.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Polyfill\Php72 as p; - -if (\PHP_VERSION_ID >= 70200) { - return; -} - -if (!defined('PHP_FLOAT_DIG')) { - define('PHP_FLOAT_DIG', 15); -} -if (!defined('PHP_FLOAT_EPSILON')) { - define('PHP_FLOAT_EPSILON', 2.2204460492503E-16); -} -if (!defined('PHP_FLOAT_MIN')) { - define('PHP_FLOAT_MIN', 2.2250738585072E-308); -} -if (!defined('PHP_FLOAT_MAX')) { - define('PHP_FLOAT_MAX', 1.7976931348623157E+308); -} -if (!defined('PHP_OS_FAMILY')) { - define('PHP_OS_FAMILY', p\Php72::php_os_family()); -} - -if ('\\' === \DIRECTORY_SEPARATOR && !function_exists('sapi_windows_vt100_support')) { - function sapi_windows_vt100_support($stream, $enable = null) { return p\Php72::sapi_windows_vt100_support($stream, $enable); } -} -if (!function_exists('stream_isatty')) { - function stream_isatty($stream) { return p\Php72::stream_isatty($stream); } -} -if (!function_exists('utf8_encode')) { - function utf8_encode($string) { return p\Php72::utf8_encode($string); } -} -if (!function_exists('utf8_decode')) { - function utf8_decode($string) { return p\Php72::utf8_decode($string); } -} -if (!function_exists('spl_object_id')) { - function spl_object_id($object) { return p\Php72::spl_object_id($object); } -} -if (!function_exists('mb_ord')) { - function mb_ord($string, $encoding = null) { return p\Php72::mb_ord($string, $encoding); } -} -if (!function_exists('mb_chr')) { - function mb_chr($codepoint, $encoding = null) { return p\Php72::mb_chr($codepoint, $encoding); } -} -if (!function_exists('mb_scrub')) { - function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } -} diff --git a/symfony/polyfill-php73/Php73.php b/symfony/polyfill-php73/Php73.php deleted file mode 100644 index 65c35a6a1..000000000 --- a/symfony/polyfill-php73/Php73.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Polyfill\Php73; - -/** - * @author Gabriel Caruso - * @author Ion Bazan - * - * @internal - */ -final class Php73 -{ - public static $startAt = 1533462603; - - /** - * @param bool $asNum - * - * @return array|float|int - */ - public static function hrtime($asNum = false) - { - $ns = microtime(false); - $s = substr($ns, 11) - self::$startAt; - $ns = 1E9 * (float) $ns; - - if ($asNum) { - $ns += $s * 1E9; - - return \PHP_INT_SIZE === 4 ? $ns : (int) $ns; - } - - return [$s, (int) $ns]; - } -} diff --git a/symfony/polyfill-php73/bootstrap.php b/symfony/polyfill-php73/bootstrap.php deleted file mode 100644 index d6b215382..000000000 --- a/symfony/polyfill-php73/bootstrap.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Polyfill\Php73 as p; - -if (\PHP_VERSION_ID >= 70300) { - return; -} - -if (!function_exists('is_countable')) { - function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; } -} -if (!function_exists('hrtime')) { - require_once __DIR__.'/Php73.php'; - p\Php73::$startAt = (int) microtime(true); - function hrtime($as_number = false) { return p\Php73::hrtime($as_number); } -} -if (!function_exists('array_key_first')) { - function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } } -} -if (!function_exists('array_key_last')) { - function array_key_last(array $array) { return key(array_slice($array, -1, 1, true)); } -} diff --git a/symfony/polyfill-php73/LICENSE b/symfony/polyfill-php83/LICENSE similarity index 95% rename from symfony/polyfill-php73/LICENSE rename to symfony/polyfill-php83/LICENSE index 7536caeae..733c826eb 100644 --- a/symfony/polyfill-php73/LICENSE +++ b/symfony/polyfill-php83/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-present Fabien Potencier +Copyright (c) 2022-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/polyfill-php83/Php83.php b/symfony/polyfill-php83/Php83.php new file mode 100644 index 000000000..3d94b6c32 --- /dev/null +++ b/symfony/polyfill-php83/Php83.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php83; + +/** + * @author Ion Bazan + * @author Pierre Ambroise + * + * @internal + */ +final class Php83 +{ + private const JSON_MAX_DEPTH = 0x7FFFFFFF; // see https://www.php.net/manual/en/function.json-decode.php + + public static function json_validate(string $json, int $depth = 512, int $flags = 0): bool + { + if (0 !== $flags && \defined('JSON_INVALID_UTF8_IGNORE') && \JSON_INVALID_UTF8_IGNORE !== $flags) { + throw new \ValueError('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)'); + } + + if ($depth <= 0) { + throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0'); + } + + if ($depth > self::JSON_MAX_DEPTH) { + throw new \ValueError(sprintf('json_validate(): Argument #2 ($depth) must be less than %d', self::JSON_MAX_DEPTH)); + } + + json_decode($json, null, $depth, $flags); + + return \JSON_ERROR_NONE === json_last_error(); + } + + public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string + { + if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { + throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + if (mb_strlen($pad_string, $encoding) <= 0) { + throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); + } + + $paddingRequired = $length - mb_strlen($string, $encoding); + + if ($paddingRequired < 1) { + return $string; + } + + switch ($pad_type) { + case \STR_PAD_LEFT: + return mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; + case \STR_PAD_RIGHT: + return $string.mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); + default: + $leftPaddingLength = floor($paddingRequired / 2); + $rightPaddingLength = $paddingRequired - $leftPaddingLength; + + return mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); + } + } + + public static function str_increment(string $string): string + { + if ('' === $string) { + throw new \ValueError('str_increment(): Argument #1 ($string) cannot be empty'); + } + + if (!preg_match('/^[a-zA-Z0-9]+$/', $string)) { + throw new \ValueError('str_increment(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters'); + } + + if (is_numeric($string)) { + $offset = stripos($string, 'e'); + if (false !== $offset) { + $char = $string[$offset]; + ++$char; + $string[$offset] = $char; + ++$string; + + switch ($string[$offset]) { + case 'f': + $string[$offset] = 'e'; + break; + case 'F': + $string[$offset] = 'E'; + break; + case 'g': + $string[$offset] = 'f'; + break; + case 'G': + $string[$offset] = 'F'; + break; + } + + return $string; + } + } + + return ++$string; + } + + public static function str_decrement(string $string): string + { + if ('' === $string) { + throw new \ValueError('str_decrement(): Argument #1 ($string) cannot be empty'); + } + + if (!preg_match('/^[a-zA-Z0-9]+$/', $string)) { + throw new \ValueError('str_decrement(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters'); + } + + if (preg_match('/\A(?:0[aA0]?|[aA])\z/', $string)) { + throw new \ValueError(sprintf('str_decrement(): Argument #1 ($string) "%s" is out of decrement range', $string)); + } + + if (!\in_array(substr($string, -1), ['A', 'a', '0'], true)) { + return implode('', \array_slice(str_split($string), 0, -1)).\chr(\ord(substr($string, -1)) - 1); + } + + $carry = ''; + $decremented = ''; + + for ($i = \strlen($string) - 1; $i >= 0; --$i) { + $char = $string[$i]; + + switch ($char) { + case 'A': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + $carry = 'Z'; + + break; + case 'a': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + $carry = 'z'; + + break; + case '0': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + $carry = '9'; + + break; + case '1': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + + break; + default: + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + + if (!\in_array($char, ['A', 'a', '0'], true)) { + $decremented = \chr(\ord($char) - 1).$decremented; + } + } + } + + return $decremented; + } +} diff --git a/symfony/polyfill-php73/Resources/stubs/JsonException.php b/symfony/polyfill-php83/Resources/stubs/DateError.php similarity index 77% rename from symfony/polyfill-php73/Resources/stubs/JsonException.php rename to symfony/polyfill-php83/Resources/stubs/DateError.php index f06d6c269..6e7ed8c86 100644 --- a/symfony/polyfill-php73/Resources/stubs/JsonException.php +++ b/symfony/polyfill-php83/Resources/stubs/DateError.php @@ -9,8 +9,8 @@ * file that was distributed with this source code. */ -if (\PHP_VERSION_ID < 70300) { - class JsonException extends Exception +if (\PHP_VERSION_ID < 80300) { + class DateError extends Error { } } diff --git a/symfony/polyfill-php83/Resources/stubs/DateException.php b/symfony/polyfill-php83/Resources/stubs/DateException.php new file mode 100644 index 000000000..041710af5 --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateException extends Exception + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php b/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php new file mode 100644 index 000000000..e2e9dfc95 --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateInvalidOperationException extends DateException + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php b/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php new file mode 100644 index 000000000..75bcd267a --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateInvalidTimeZoneException extends DateException + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php b/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php new file mode 100644 index 000000000..af91b8e4d --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateMalformedIntervalStringException extends DateException + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php b/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php new file mode 100644 index 000000000..9b6d2764e --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateMalformedPeriodStringException extends DateException + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php b/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php new file mode 100644 index 000000000..7ad04849f --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateMalformedStringException extends DateException + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateObjectError.php b/symfony/polyfill-php83/Resources/stubs/DateObjectError.php new file mode 100644 index 000000000..11f0edc68 --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateObjectError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateObjectError extends DateError + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/DateRangeError.php b/symfony/polyfill-php83/Resources/stubs/DateRangeError.php new file mode 100644 index 000000000..98e67036e --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/DateRangeError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateRangeError extends DateError + { + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/Override.php b/symfony/polyfill-php83/Resources/stubs/Override.php new file mode 100644 index 000000000..d3e6b3e1d --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/Override.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + #[Attribute(Attribute::TARGET_METHOD)] + final class Override + { + public function __construct() + { + } + } +} diff --git a/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php b/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php new file mode 100644 index 000000000..ecb7c98e0 --- /dev/null +++ b/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class SQLite3Exception extends Exception + { + } +} diff --git a/symfony/polyfill-php83/bootstrap.php b/symfony/polyfill-php83/bootstrap.php new file mode 100644 index 000000000..a92799cb3 --- /dev/null +++ b/symfony/polyfill-php83/bootstrap.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php83 as p; + +if (\PHP_VERSION_ID >= 80300) { + return; +} + +if (!function_exists('json_validate')) { + function json_validate(string $json, int $depth = 512, int $flags = 0): bool { return p\Php83::json_validate($json, $depth, $flags); } +} + +if (extension_loaded('mbstring')) { + if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } + } +} + +if (!function_exists('stream_context_set_options')) { + function stream_context_set_options($context, array $options): bool { return stream_context_set_option($context, $options); } +} + +if (!function_exists('str_increment')) { + function str_increment(string $string): string { return p\Php83::str_increment($string); } +} + +if (!function_exists('str_decrement')) { + function str_decrement(string $string): string { return p\Php83::str_decrement($string); } +} + +if (\PHP_VERSION_ID >= 80100) { + return require __DIR__.'/bootstrap81.php'; +} + +if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) { + function ldap_exop_sync($ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $controls, $response_data, $response_oid); } +} + +if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) { + function ldap_connect_wallet(?string $uri, string $wallet, string $password, int $auth_mode = \GSLC_SSL_NO_AUTH) { return ldap_connect($uri, $wallet, $password, $auth_mode); } +} diff --git a/symfony/polyfill-php83/bootstrap81.php b/symfony/polyfill-php83/bootstrap81.php new file mode 100644 index 000000000..68395b439 --- /dev/null +++ b/symfony/polyfill-php83/bootstrap81.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID >= 80300) { + return; +} + +if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) { + function ldap_exop_sync(\LDAP\Connection $ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $controls, $response_data, $response_oid); } +} + +if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) { + function ldap_connect_wallet(?string $uri, string $wallet, #[\SensitiveParameter] string $password, int $auth_mode = \GSLC_SSL_NO_AUTH): \LDAP\Connection|false { return ldap_connect($uri, $wallet, $password, $auth_mode); } +} diff --git a/symfony/process/Exception/ProcessFailedException.php b/symfony/process/Exception/ProcessFailedException.php index 328acfde5..19b40570c 100644 --- a/symfony/process/Exception/ProcessFailedException.php +++ b/symfony/process/Exception/ProcessFailedException.php @@ -20,7 +20,7 @@ */ class ProcessFailedException extends RuntimeException { - private $process; + private Process $process; public function __construct(Process $process) { @@ -47,6 +47,9 @@ public function __construct(Process $process) $this->process = $process; } + /** + * @return Process + */ public function getProcess() { return $this->process; diff --git a/symfony/process/Exception/ProcessSignaledException.php b/symfony/process/Exception/ProcessSignaledException.php index d4d322756..0fed8ac30 100644 --- a/symfony/process/Exception/ProcessSignaledException.php +++ b/symfony/process/Exception/ProcessSignaledException.php @@ -20,7 +20,7 @@ */ final class ProcessSignaledException extends RuntimeException { - private $process; + private Process $process; public function __construct(Process $process) { diff --git a/symfony/process/Exception/ProcessTimedOutException.php b/symfony/process/Exception/ProcessTimedOutException.php index 94391a459..1cecdae75 100644 --- a/symfony/process/Exception/ProcessTimedOutException.php +++ b/symfony/process/Exception/ProcessTimedOutException.php @@ -23,8 +23,8 @@ class ProcessTimedOutException extends RuntimeException public const TYPE_GENERAL = 1; public const TYPE_IDLE = 2; - private $process; - private $timeoutType; + private Process $process; + private int $timeoutType; public function __construct(Process $process, int $timeoutType) { @@ -38,32 +38,36 @@ public function __construct(Process $process, int $timeoutType) )); } + /** + * @return Process + */ public function getProcess() { return $this->process; } + /** + * @return bool + */ public function isGeneralTimeout() { return self::TYPE_GENERAL === $this->timeoutType; } + /** + * @return bool + */ public function isIdleTimeout() { return self::TYPE_IDLE === $this->timeoutType; } - public function getExceededTimeout() + public function getExceededTimeout(): ?float { - switch ($this->timeoutType) { - case self::TYPE_GENERAL: - return $this->process->getTimeout(); - - case self::TYPE_IDLE: - return $this->process->getIdleTimeout(); - - default: - throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); - } + return match ($this->timeoutType) { + self::TYPE_GENERAL => $this->process->getTimeout(), + self::TYPE_IDLE => $this->process->getIdleTimeout(), + default => throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)), + }; } } diff --git a/symfony/process/Exception/RunProcessFailedException.php b/symfony/process/Exception/RunProcessFailedException.php new file mode 100644 index 000000000..e7219d351 --- /dev/null +++ b/symfony/process/Exception/RunProcessFailedException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Messenger\RunProcessContext; + +/** + * @author Kevin Bond + */ +final class RunProcessFailedException extends RuntimeException +{ + public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context) + { + parent::__construct($exception->getMessage(), $exception->getCode()); + } +} diff --git a/symfony/process/ExecutableFinder.php b/symfony/process/ExecutableFinder.php index eb8f06292..8c7bf58d0 100644 --- a/symfony/process/ExecutableFinder.php +++ b/symfony/process/ExecutableFinder.php @@ -19,10 +19,12 @@ */ class ExecutableFinder { - private $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private array $suffixes = ['.exe', '.bat', '.cmd', '.com']; /** * Replaces default suffixes of executable. + * + * @return void */ public function setSuffixes(array $suffixes) { @@ -31,6 +33,8 @@ public function setSuffixes(array $suffixes) /** * Adds new possible suffix to check for executable. + * + * @return void */ public function addSuffix(string $suffix) { @@ -43,30 +47,13 @@ public function addSuffix(string $suffix) * @param string $name The executable name (without the extension) * @param string|null $default The default to return if no executable is found * @param array $extraDirs Additional dirs to check into - * - * @return string|null */ - public function find(string $name, string $default = null, array $extraDirs = []) + public function find(string $name, ?string $default = null, array $extraDirs = []): ?string { - if (\ini_get('open_basedir')) { - $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); - $dirs = []; - foreach ($searchPath as $path) { - // Silencing against https://bugs.php.net/69240 - if (@is_dir($path)) { - $dirs[] = $path; - } else { - if (basename($path) == $name && @is_executable($path)) { - return $path; - } - } - } - } else { - $dirs = array_merge( - explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), - $extraDirs - ); - } + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { @@ -78,9 +65,18 @@ public function find(string $name, string $default = null, array $extraDirs = [] if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } + + if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { + return $dir; + } } } + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { + return $executablePath; + } + return $default; } } diff --git a/symfony/process/InputStream.php b/symfony/process/InputStream.php index 240665f32..931217c85 100644 --- a/symfony/process/InputStream.php +++ b/symfony/process/InputStream.php @@ -22,17 +22,18 @@ */ class InputStream implements \IteratorAggregate { - /** @var callable|null */ - private $onEmpty = null; - private $input = []; - private $open = true; + private ?\Closure $onEmpty = null; + private array $input = []; + private bool $open = true; /** * Sets a callback that is called when the write buffer becomes empty. + * + * @return void */ - public function onEmpty(callable $onEmpty = null) + public function onEmpty(?callable $onEmpty = null) { - $this->onEmpty = $onEmpty; + $this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null; } /** @@ -40,8 +41,10 @@ public function onEmpty(callable $onEmpty = null) * * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, * stream resource or \Traversable + * + * @return void */ - public function write($input) + public function write(mixed $input) { if (null === $input) { return; @@ -54,6 +57,8 @@ public function write($input) /** * Closes the write buffer. + * + * @return void */ public function close() { @@ -62,17 +67,15 @@ public function close() /** * Tells whether the write buffer is closed or not. + * + * @return bool */ public function isClosed() { return !$this->open; } - /** - * @return \Traversable - */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { $this->open = true; diff --git a/symfony/process/Messenger/RunProcessContext.php b/symfony/process/Messenger/RunProcessContext.php new file mode 100644 index 000000000..b5ade0722 --- /dev/null +++ b/symfony/process/Messenger/RunProcessContext.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessContext +{ + public readonly ?int $exitCode; + public readonly ?string $output; + public readonly ?string $errorOutput; + + public function __construct( + public readonly RunProcessMessage $message, + Process $process, + ) { + $this->exitCode = $process->getExitCode(); + $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); + } +} diff --git a/symfony/process/Messenger/RunProcessMessage.php b/symfony/process/Messenger/RunProcessMessage.php new file mode 100644 index 000000000..b2c33fe3b --- /dev/null +++ b/symfony/process/Messenger/RunProcessMessage.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +/** + * @author Kevin Bond + */ +class RunProcessMessage implements \Stringable +{ + public function __construct( + public readonly array $command, + public readonly ?string $cwd = null, + public readonly ?array $env = null, + public readonly mixed $input = null, + public readonly ?float $timeout = 60.0, + ) { + } + + public function __toString(): string + { + return implode(' ', $this->command); + } +} diff --git a/symfony/process/Messenger/RunProcessMessageHandler.php b/symfony/process/Messenger/RunProcessMessageHandler.php new file mode 100644 index 000000000..41c1934cc --- /dev/null +++ b/symfony/process/Messenger/RunProcessMessageHandler.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\RunProcessFailedException; +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessMessageHandler +{ + public function __invoke(RunProcessMessage $message): RunProcessContext + { + $process = new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout); + + try { + return new RunProcessContext($message, $process->mustRun()); + } catch (ProcessFailedException $e) { + throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess())); + } + } +} diff --git a/symfony/process/PhpExecutableFinder.php b/symfony/process/PhpExecutableFinder.php index bed6c3dc8..4a882e0f2 100644 --- a/symfony/process/PhpExecutableFinder.php +++ b/symfony/process/PhpExecutableFinder.php @@ -19,7 +19,7 @@ */ class PhpExecutableFinder { - private $executableFinder; + private ExecutableFinder $executableFinder; public function __construct() { @@ -28,15 +28,13 @@ public function __construct() /** * Finds The PHP executable. - * - * @return string|false */ - public function find(bool $includeArgs = true) + public function find(bool $includeArgs = true): string|false { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; - if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; + if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; } @@ -88,10 +86,8 @@ public function find(bool $includeArgs = true) /** * Finds the PHP executable arguments. - * - * @return array */ - public function findArguments() + public function findArguments(): array { $arguments = []; if ('phpdbg' === \PHP_SAPI) { diff --git a/symfony/process/PhpProcess.php b/symfony/process/PhpProcess.php index 2bc338e5e..6e2ab59fb 100644 --- a/symfony/process/PhpProcess.php +++ b/symfony/process/PhpProcess.php @@ -32,7 +32,7 @@ class PhpProcess extends Process * @param int $timeout The timeout in seconds * @param array|null $php Path to the PHP binary to use with any additional arguments */ - public function __construct(string $script, string $cwd = null, array $env = null, int $timeout = 60, array $php = null) + public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) { if (null === $php) { $executableFinder = new PhpExecutableFinder(); @@ -50,18 +50,15 @@ public function __construct(string $script, string $cwd = null, array $env = nul parent::__construct($php, $cwd, $env, $script, $timeout); } - /** - * {@inheritdoc} - */ - public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } /** - * {@inheritdoc} + * @return void */ - public function start(callable $callback = null, array $env = []) + public function start(?callable $callback = null, array $env = []) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); diff --git a/symfony/process/PhpSubprocess.php b/symfony/process/PhpSubprocess.php new file mode 100644 index 000000000..04fd8ea87 --- /dev/null +++ b/symfony/process/PhpSubprocess.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings. + * + * For this, it generates a temporary php.ini file taking over all the current settings and disables + * loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini". + * + * Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content: + * + * run(); + * print $p->getOutput()."\n"; + * + * This will output "string(2) "-1", because the process is started with the default php.ini settings. + * + * $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']); + * $p->run(); + * print $p->getOutput()."\n"; + * + * This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings. + * + * @author Yanick Witschi + * @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson + */ +class PhpSubprocess extends Process +{ + /** + * @param array $command The command to run and its arguments listed as separate entries. They will automatically + * get prefixed with the PHP binary + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array|null $php Path to the PHP binary to use with any additional arguments + */ + public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) + { + if (null === $php) { + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); + } + + if (null === $php) { + throw new RuntimeException('Unable to find PHP binary.'); + } + + $tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir()); + + $php = array_merge($php, ['-n', '-c', $tmpIni]); + register_shutdown_function('unlink', $tmpIni); + + $command = array_merge($php, $command); + + parent::__construct($command, $cwd, $env, null, $timeout); + } + + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + } + + public function start(?callable $callback = null, array $env = []): void + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback, $env); + } + + private function writeTmpIni(array $iniFiles, string $tmpDir): string + { + if (false === $tmpfile = @tempnam($tmpDir, '')) { + throw new RuntimeException('Unable to create temporary ini file.'); + } + + // $iniFiles has at least one item and it may be empty + if ('' === $iniFiles[0]) { + array_shift($iniFiles); + } + + $content = ''; + + foreach ($iniFiles as $file) { + // Check for inaccessible ini files + if (($data = @file_get_contents($file)) === false) { + throw new RuntimeException('Unable to read ini: '.$file); + } + // Check and remove directives after HOST and PATH sections + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { + $data = substr($data, 0, $matches[0][1]); + } + + $content .= $data."\n"; + } + + // Merge loaded settings into our ini content, if it is valid + $config = parse_ini_string($content); + $loaded = ini_get_all(null, false); + + if (false === $config || false === $loaded) { + throw new RuntimeException('Unable to parse ini data.'); + } + + $content .= $this->mergeLoadedConfig($loaded, $config); + + // Work-around for https://bugs.php.net/bug.php?id=75932 + $content .= "opcache.enable_cli=0\n"; + + if (false === @file_put_contents($tmpfile, $content)) { + throw new RuntimeException('Unable to write temporary ini file.'); + } + + return $tmpfile; + } + + private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string + { + $content = ''; + + foreach ($loadedConfig as $name => $value) { + if (!\is_string($value)) { + continue; + } + + if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { + // Double-quote escape each value + $content .= $name.'="'.addcslashes($value, '\\"')."\"\n"; + } + } + + return $content; + } + + private function getAllIniFiles(): array + { + $paths = [(string) php_ini_loaded_file()]; + + if (false !== $scanned = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); + } + + return $paths; + } +} diff --git a/symfony/process/Pipes/AbstractPipes.php b/symfony/process/Pipes/AbstractPipes.php index 656dc0328..cbbb72770 100644 --- a/symfony/process/Pipes/AbstractPipes.php +++ b/symfony/process/Pipes/AbstractPipes.php @@ -20,31 +20,27 @@ */ abstract class AbstractPipes implements PipesInterface { - public $pipes = []; + public array $pipes = []; - private $inputBuffer = ''; + private string $inputBuffer = ''; + /** @var resource|string|\Iterator */ private $input; - private $blocked = true; - private $lastError; + private bool $blocked = true; + private ?string $lastError = null; /** - * @param resource|string|int|float|bool|\Iterator|null $input + * @param resource|string|\Iterator $input */ public function __construct($input) { if (\is_resource($input) || $input instanceof \Iterator) { $this->input = $input; - } elseif (\is_string($input)) { - $this->inputBuffer = $input; } else { $this->inputBuffer = (string) $input; } } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { foreach ($this->pipes as $pipe) { if (\is_resource($pipe)) { @@ -69,7 +65,7 @@ protected function hasSystemCallBeenInterrupted(): bool /** * Unblocks streams. */ - protected function unblock() + protected function unblock(): void { if (!$this->blocked) { return; @@ -173,7 +169,7 @@ protected function write(): ?array /** * @internal */ - public function handleError(int $type, string $msg) + public function handleError(int $type, string $msg): void { $this->lastError = $msg; } diff --git a/symfony/process/Pipes/PipesInterface.php b/symfony/process/Pipes/PipesInterface.php index 50eb5c47e..967f8de7f 100644 --- a/symfony/process/Pipes/PipesInterface.php +++ b/symfony/process/Pipes/PipesInterface.php @@ -57,5 +57,5 @@ public function haveReadSupport(): bool; /** * Closes file handles and pipes. */ - public function close(); + public function close(): void; } diff --git a/symfony/process/Pipes/UnixPipes.php b/symfony/process/Pipes/UnixPipes.php index 5a0e9d47f..7bd0db0e9 100644 --- a/symfony/process/Pipes/UnixPipes.php +++ b/symfony/process/Pipes/UnixPipes.php @@ -22,11 +22,11 @@ */ class UnixPipes extends AbstractPipes { - private $ttyMode; - private $ptyMode; - private $haveReadSupport; + private ?bool $ttyMode; + private bool $ptyMode; + private bool $haveReadSupport; - public function __construct(?bool $ttyMode, bool $ptyMode, $input, bool $haveReadSupport) + public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) { $this->ttyMode = $ttyMode; $this->ptyMode = $ptyMode; @@ -40,7 +40,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } @@ -50,9 +50,6 @@ public function __destruct() $this->close(); } - /** - * {@inheritdoc} - */ public function getDescriptors(): array { if (!$this->haveReadSupport) { @@ -88,17 +85,11 @@ public function getDescriptors(): array ]; } - /** - * {@inheritdoc} - */ public function getFiles(): array { return []; } - /** - * {@inheritdoc} - */ public function readAndWrite(bool $blocking, bool $close = false): array { $this->unblock(); @@ -109,7 +100,7 @@ public function readAndWrite(bool $blocking, bool $close = false): array unset($r[0]); // let's have a look if something changed in streams - set_error_handler([$this, 'handleError']); + set_error_handler($this->handleError(...)); if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { restore_error_handler(); // if a system call has been interrupted, forget about it, let's try again @@ -145,17 +136,11 @@ public function readAndWrite(bool $blocking, bool $close = false): array return $read; } - /** - * {@inheritdoc} - */ public function haveReadSupport(): bool { return $this->haveReadSupport; } - /** - * {@inheritdoc} - */ public function areOpen(): bool { return (bool) $this->pipes; diff --git a/symfony/process/Pipes/WindowsPipes.php b/symfony/process/Pipes/WindowsPipes.php index 968dd0262..8033442a4 100644 --- a/symfony/process/Pipes/WindowsPipes.php +++ b/symfony/process/Pipes/WindowsPipes.php @@ -26,16 +26,16 @@ */ class WindowsPipes extends AbstractPipes { - private $files = []; - private $fileHandles = []; - private $lockHandles = []; - private $readBytes = [ + private array $files = []; + private array $fileHandles = []; + private array $lockHandles = []; + private array $readBytes = [ Process::STDOUT => 0, Process::STDERR => 0, ]; - private $haveReadSupport; + private bool $haveReadSupport; - public function __construct($input, bool $haveReadSupport) + public function __construct(mixed $input, bool $haveReadSupport) { $this->haveReadSupport = $haveReadSupport; @@ -93,7 +93,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } @@ -103,9 +103,6 @@ public function __destruct() $this->close(); } - /** - * {@inheritdoc} - */ public function getDescriptors(): array { if (!$this->haveReadSupport) { @@ -128,17 +125,11 @@ public function getDescriptors(): array ]; } - /** - * {@inheritdoc} - */ public function getFiles(): array { return $this->files; } - /** - * {@inheritdoc} - */ public function readAndWrite(bool $blocking, bool $close = false): array { $this->unblock(); @@ -171,26 +162,17 @@ public function readAndWrite(bool $blocking, bool $close = false): array return $read; } - /** - * {@inheritdoc} - */ public function haveReadSupport(): bool { return $this->haveReadSupport; } - /** - * {@inheritdoc} - */ public function areOpen(): bool { return $this->pipes && $this->fileHandles; } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { parent::close(); foreach ($this->fileHandles as $type => $handle) { diff --git a/symfony/process/Process.php b/symfony/process/Process.php index 30ebeb6b5..b36d434e4 100644 --- a/symfony/process/Process.php +++ b/symfony/process/Process.php @@ -17,7 +17,6 @@ use Symfony\Component\Process\Exception\ProcessSignaledException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\Pipes\PipesInterface; use Symfony\Component\Process\Pipes\UnixPipes; use Symfony\Component\Process\Pipes\WindowsPipes; @@ -51,37 +50,39 @@ class Process implements \IteratorAggregate public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating - private $callback; - private $hasCallback = false; - private $commandline; - private $cwd; - private $env = []; + private ?\Closure $callback = null; + private array|string $commandline; + private ?string $cwd; + private array $env = []; + /** @var resource|string|\Iterator|null */ private $input; - private $starttime; - private $lastOutputTime; - private $timeout; - private $idleTimeout; - private $exitcode; - private $fallbackStatus = []; - private $processInformation; - private $outputDisabled = false; + private ?float $starttime = null; + private ?float $lastOutputTime = null; + private ?float $timeout = null; + private ?float $idleTimeout = null; + private ?int $exitcode = null; + private array $fallbackStatus = []; + private array $processInformation; + private bool $outputDisabled = false; + /** @var resource */ private $stdout; + /** @var resource */ private $stderr; + /** @var resource|null */ private $process; - private $status = self::STATUS_READY; - private $incrementalOutputOffset = 0; - private $incrementalErrorOutputOffset = 0; - private $tty = false; - private $pty; - private $options = ['suppress_errors' => true, 'bypass_shell' => true]; + private string $status = self::STATUS_READY; + private int $incrementalOutputOffset = 0; + private int $incrementalErrorOutputOffset = 0; + private bool $tty = false; + private bool $pty; + private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; - private $useFileHandles = false; - /** @var PipesInterface */ - private $processPipes; + private WindowsPipes|UnixPipes $processPipes; - private $latestSignal; + private ?int $latestSignal = null; + private ?int $cachedExitCode = null; - private static $sigchild; + private static ?bool $sigchild = null; /** * Exit codes translation table. @@ -140,7 +141,7 @@ class Process implements \IteratorAggregate * * @throws LogicException When proc_open is not installed */ - public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60) { if (!\function_exists('proc_open')) { throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); @@ -162,7 +163,6 @@ public function __construct(array $command, string $cwd = null, array $env = nul $this->setInput($input); $this->setTimeout($timeout); - $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR; $this->pty = false; } @@ -185,11 +185,9 @@ public function __construct(array $command, string $cwd = null, array $env = nul * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * - * @return static - * * @throws LogicException When proc_open is not installed */ - public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { $process = new static([], $cwd, $env, $input, $timeout); $process->commandline = $command; @@ -197,14 +195,14 @@ public static function fromShellCommandline(string $command, string $cwd = null, return $process; } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } + /** + * @return void + */ public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); @@ -247,7 +245,7 @@ public function __clone() * * @final */ - public function run(callable $callback = null, array $env = []): int + public function run(?callable $callback = null, array $env = []): int { $this->start($callback, $env); @@ -266,7 +264,7 @@ public function run(callable $callback = null, array $env = []): int * * @final */ - public function mustRun(callable $callback = null, array $env = []): self + public function mustRun(?callable $callback = null, array $env = []): static { if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); @@ -290,11 +288,13 @@ public function mustRun(callable $callback = null, array $env = []): self * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * + * @return void + * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ - public function start(callable $callback = null, array $env = []) + public function start(?callable $callback = null, array $env = []) { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -303,8 +303,7 @@ public function start(callable $callback = null, array $env = []) $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); - $this->hasCallback = null !== $callback; - $descriptors = $this->getDescriptors(); + $descriptors = $this->getDescriptors(null !== $callback); if ($this->env) { $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; @@ -313,7 +312,7 @@ public function start(callable $callback = null, array $env = []) $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); if (\is_array($commandline = $this->commandline)) { - $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); + $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline)); if ('\\' !== \DIRECTORY_SEPARATOR) { // exec is mandatory to deal with sending a signal to the process @@ -325,17 +324,13 @@ public function start(callable $callback = null, array $env = []) if ('\\' === \DIRECTORY_SEPARATOR) { $commandline = $this->prepareWindowsCommandLine($commandline, $env); - } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) { + } elseif ($this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; - - // Workaround for the bug, when PTS functionality is enabled. - // @see : https://bugs.php.net/69442 - $ptsWorkaround = fopen(__FILE__, 'r'); } $envPairs = []; @@ -349,11 +344,12 @@ public function start(callable $callback = null, array $env = []) throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - if (!\is_resource($this->process)) { + if (!$process) { throw new RuntimeException('Unable to launch a new process.'); } + $this->process = $process; $this->status = self::STATUS_STARTED; if (isset($descriptors[3])) { @@ -376,8 +372,6 @@ public function start(callable $callback = null, array $env = []) * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @return static - * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @@ -385,7 +379,7 @@ public function start(callable $callback = null, array $env = []) * * @final */ - public function restart(callable $callback = null, array $env = []): self + public function restart(?callable $callback = null, array $env = []): static { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -412,7 +406,7 @@ public function restart(callable $callback = null, array $env = []): self * @throws ProcessSignaledException When process stopped after receiving signal * @throws LogicException When process is not yet started */ - public function wait(callable $callback = null) + public function wait(?callable $callback = null): int { $this->requireProcessIsStarted(__FUNCTION__); @@ -495,7 +489,7 @@ public function waitUntil(callable $callback): bool * * @return int|null The process id if running, null otherwise */ - public function getPid() + public function getPid(): ?int { return $this->isRunning() ? $this->processInformation['pid'] : null; } @@ -511,7 +505,7 @@ public function getPid() * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ - public function signal(int $signal) + public function signal(int $signal): static { $this->doSignal($signal, true); @@ -526,7 +520,7 @@ public function signal(int $signal) * @throws RuntimeException In case the process is already running * @throws LogicException if an idle timeout is set */ - public function disableOutput() + public function disableOutput(): static { if ($this->isRunning()) { throw new RuntimeException('Disabling output while the process is running is not possible.'); @@ -547,7 +541,7 @@ public function disableOutput() * * @throws RuntimeException In case the process is already running */ - public function enableOutput() + public function enableOutput(): static { if ($this->isRunning()) { throw new RuntimeException('Enabling output while the process is running is not possible.'); @@ -560,10 +554,8 @@ public function enableOutput() /** * Returns true in case the output is disabled, false otherwise. - * - * @return bool */ - public function isOutputDisabled() + public function isOutputDisabled(): bool { return $this->outputDisabled; } @@ -571,12 +563,10 @@ public function isOutputDisabled() /** * Returns the current output of the process (STDOUT). * - * @return string - * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getOutput() + public function getOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -593,12 +583,10 @@ public function getOutput() * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * - * @return string - * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getIncrementalOutput() + public function getIncrementalOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -622,8 +610,7 @@ public function getIncrementalOutput() * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - #[\ReturnTypeWillChange] - public function getIterator(int $flags = 0) + public function getIterator(int $flags = 0): \Generator { $this->readPipesForOutput(__FUNCTION__, false); @@ -675,7 +662,7 @@ public function getIterator(int $flags = 0) * * @return $this */ - public function clearOutput() + public function clearOutput(): static { ftruncate($this->stdout, 0); fseek($this->stdout, 0); @@ -687,12 +674,10 @@ public function clearOutput() /** * Returns the current error output of the process (STDERR). * - * @return string - * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getErrorOutput() + public function getErrorOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -710,12 +695,10 @@ public function getErrorOutput() * whole error output, this one returns the new error output since the last * call. * - * @return string - * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getIncrementalErrorOutput() + public function getIncrementalErrorOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -734,7 +717,7 @@ public function getIncrementalErrorOutput() * * @return $this */ - public function clearErrorOutput() + public function clearErrorOutput(): static { ftruncate($this->stderr, 0); fseek($this->stderr, 0); @@ -748,7 +731,7 @@ public function clearErrorOutput() * * @return int|null The exit status code, null if the Process is not terminated */ - public function getExitCode() + public function getExitCode(): ?int { $this->updateStatus(false); @@ -766,7 +749,7 @@ public function getExitCode() * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ - public function getExitCodeText() + public function getExitCodeText(): ?string { if (null === $exitcode = $this->getExitCode()) { return null; @@ -777,10 +760,8 @@ public function getExitCodeText() /** * Checks if the process ended successfully. - * - * @return bool */ - public function isSuccessful() + public function isSuccessful(): bool { return 0 === $this->getExitCode(); } @@ -790,11 +771,9 @@ public function isSuccessful() * * It always returns false on Windows. * - * @return bool - * * @throws LogicException In case the process is not terminated */ - public function hasBeenSignaled() + public function hasBeenSignaled(): bool { $this->requireProcessIsTerminated(__FUNCTION__); @@ -806,12 +785,10 @@ public function hasBeenSignaled() * * It is only meaningful if hasBeenSignaled() returns true. * - * @return int - * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ - public function getTermSignal() + public function getTermSignal(): int { $this->requireProcessIsTerminated(__FUNCTION__); @@ -827,11 +804,9 @@ public function getTermSignal() * * It always returns false on Windows. * - * @return bool - * * @throws LogicException In case the process is not terminated */ - public function hasBeenStopped() + public function hasBeenStopped(): bool { $this->requireProcessIsTerminated(__FUNCTION__); @@ -843,11 +818,9 @@ public function hasBeenStopped() * * It is only meaningful if hasBeenStopped() returns true. * - * @return int - * * @throws LogicException In case the process is not terminated */ - public function getStopSignal() + public function getStopSignal(): int { $this->requireProcessIsTerminated(__FUNCTION__); @@ -856,10 +829,8 @@ public function getStopSignal() /** * Checks if the process is currently running. - * - * @return bool */ - public function isRunning() + public function isRunning(): bool { if (self::STATUS_STARTED !== $this->status) { return false; @@ -872,20 +843,16 @@ public function isRunning() /** * Checks if the process has been started with no regard to the current state. - * - * @return bool */ - public function isStarted() + public function isStarted(): bool { return self::STATUS_READY != $this->status; } /** * Checks if the process is terminated. - * - * @return bool */ - public function isTerminated() + public function isTerminated(): bool { $this->updateStatus(false); @@ -896,10 +863,8 @@ public function isTerminated() * Gets the process status. * * The status is one of: ready, started, terminated. - * - * @return string */ - public function getStatus() + public function getStatus(): string { $this->updateStatus(false); @@ -914,7 +879,7 @@ public function getStatus() * * @return int|null The exit-code of the process or null if it's not running */ - public function stop(float $timeout = 10, int $signal = null) + public function stop(float $timeout = 10, ?int $signal = null): ?int { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { @@ -948,7 +913,7 @@ public function stop(float $timeout = 10, int $signal = null) * * @internal */ - public function addOutput(string $line) + public function addOutput(string $line): void { $this->lastOutputTime = microtime(true); @@ -962,7 +927,7 @@ public function addOutput(string $line) * * @internal */ - public function addErrorOutput(string $line) + public function addErrorOutput(string $line): void { $this->lastOutputTime = microtime(true); @@ -981,30 +946,24 @@ public function getLastOutputTime(): ?float /** * Gets the command line to be executed. - * - * @return string */ - public function getCommandLine() + public function getCommandLine(): string { - return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; + return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; } /** * Gets the process timeout in seconds (max. runtime). - * - * @return float|null */ - public function getTimeout() + public function getTimeout(): ?float { return $this->timeout; } /** * Gets the process idle timeout in seconds (max. time since last output). - * - * @return float|null */ - public function getIdleTimeout() + public function getIdleTimeout(): ?float { return $this->idleTimeout; } @@ -1018,7 +977,7 @@ public function getIdleTimeout() * * @throws InvalidArgumentException if the timeout is negative */ - public function setTimeout(?float $timeout) + public function setTimeout(?float $timeout): static { $this->timeout = $this->validateTimeout($timeout); @@ -1035,7 +994,7 @@ public function setTimeout(?float $timeout) * @throws LogicException if the output is disabled * @throws InvalidArgumentException if the timeout is negative */ - public function setIdleTimeout(?float $timeout) + public function setIdleTimeout(?float $timeout): static { if (null !== $timeout && $this->outputDisabled) { throw new LogicException('Idle timeout cannot be set while the output is disabled.'); @@ -1053,7 +1012,7 @@ public function setIdleTimeout(?float $timeout) * * @throws RuntimeException In case the TTY mode is not supported */ - public function setTty(bool $tty) + public function setTty(bool $tty): static { if ('\\' === \DIRECTORY_SEPARATOR && $tty) { throw new RuntimeException('TTY mode is not supported on Windows platform.'); @@ -1070,10 +1029,8 @@ public function setTty(bool $tty) /** * Checks if the TTY mode is enabled. - * - * @return bool */ - public function isTty() + public function isTty(): bool { return $this->tty; } @@ -1083,7 +1040,7 @@ public function isTty() * * @return $this */ - public function setPty(bool $bool) + public function setPty(bool $bool): static { $this->pty = $bool; @@ -1092,20 +1049,16 @@ public function setPty(bool $bool) /** * Returns PTY state. - * - * @return bool */ - public function isPty() + public function isPty(): bool { return $this->pty; } /** * Gets the working directory. - * - * @return string|null */ - public function getWorkingDirectory() + public function getWorkingDirectory(): ?string { if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have @@ -1121,7 +1074,7 @@ public function getWorkingDirectory() * * @return $this */ - public function setWorkingDirectory(string $cwd) + public function setWorkingDirectory(string $cwd): static { $this->cwd = $cwd; @@ -1130,10 +1083,8 @@ public function setWorkingDirectory(string $cwd) /** * Gets the environment variables. - * - * @return array */ - public function getEnv() + public function getEnv(): array { return $this->env; } @@ -1145,7 +1096,7 @@ public function getEnv() * * @return $this */ - public function setEnv(array $env) + public function setEnv(array $env): static { $this->env = $env; @@ -1167,13 +1118,13 @@ public function getInput() * * This content will be passed to the underlying process standard input. * - * @param string|int|float|bool|resource|\Traversable|null $input The content + * @param string|resource|\Traversable|self|null $input The content * * @return $this * * @throws LogicException In case the process is running */ - public function setInput($input) + public function setInput(mixed $input): static { if ($this->isRunning()) { throw new LogicException('Input cannot be set while the process is running.'); @@ -1190,6 +1141,8 @@ public function setInput($input) * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * + * @return void + * * @throws ProcessTimedOutException In case the timeout was reached */ public function checkTimeout() @@ -1230,6 +1183,8 @@ public function getStartTime(): float * * Enabling the "create_new_console" option allows a subprocess to continue * to run after the main process exited, on both Windows and *nix + * + * @return void */ public function setOptions(array $options) { @@ -1256,19 +1211,13 @@ public static function isTtySupported(): bool { static $isTtySupported; - if (null === $isTtySupported) { - $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); - } - - return $isTtySupported; + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); } /** * Returns whether PTY is supported on the current operating system. - * - * @return bool */ - public static function isPtySupported() + public static function isPtySupported(): bool { static $result; @@ -1286,15 +1235,15 @@ public static function isPtySupported() /** * Creates the descriptors needed by the proc_open. */ - private function getDescriptors(): array + private function getDescriptors(bool $hasCallback): array { if ($this->input instanceof \Iterator) { $this->input->rewind(); } if ('\\' === \DIRECTORY_SEPARATOR) { - $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); + $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback); } else { - $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); + $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback); } return $this->processPipes->getDescriptors(); @@ -1307,15 +1256,11 @@ private function getDescriptors(): array * the user callback (if present) with the received output. * * @param callable|null $callback The user defined PHP callback - * - * @return \Closure */ - protected function buildCallback(callable $callback = null) + protected function buildCallback(?callable $callback = null): \Closure { if ($this->outputDisabled) { - return function ($type, $data) use ($callback): bool { - return null !== $callback && $callback($type, $data); - }; + return fn ($type, $data): bool => null !== $callback && $callback($type, $data); } $out = self::OUT; @@ -1335,6 +1280,8 @@ protected function buildCallback(callable $callback = null) * Updates the status of the process, reads pipes. * * @param bool $blocking Whether to use a blocking read call + * + * @return void */ protected function updateStatus(bool $blocking) { @@ -1345,6 +1292,19 @@ protected function updateStatus(bool $blocking) $this->processInformation = proc_get_status($this->process); $running = $this->processInformation['running']; + // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. + // Subsequent calls return -1 as the process is discarded. This workaround caches the first + // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior. + if (\PHP_VERSION_ID < 80300) { + if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) { + $this->cachedExitCode = $this->processInformation['exitcode']; + } + + if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) { + $this->processInformation['exitcode'] = $this->cachedExitCode; + } + } + $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); if ($this->fallbackStatus && $this->isSigchildEnabled()) { @@ -1358,10 +1318,8 @@ protected function updateStatus(bool $blocking) /** * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. - * - * @return bool */ - protected function isSigchildEnabled() + protected function isSigchildEnabled(): bool { if (null !== self::$sigchild) { return self::$sigchild; @@ -1385,7 +1343,7 @@ protected function isSigchildEnabled() * * @throws LogicException in case output has been disabled or process is not started */ - private function readPipesForOutput(string $caller, bool $blocking = false) + private function readPipesForOutput(string $caller, bool $blocking = false): void { if ($this->outputDisabled) { throw new LogicException('Output has been disabled.'); @@ -1420,7 +1378,7 @@ private function validateTimeout(?float $timeout): ?float * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close file handles or not */ - private function readPipes(bool $blocking, bool $close) + private function readPipes(bool $blocking, bool $close): void { $result = $this->processPipes->readAndWrite($blocking, $close); @@ -1442,8 +1400,9 @@ private function readPipes(bool $blocking, bool $close) private function close(): int { $this->processPipes->close(); - if (\is_resource($this->process)) { + if ($this->process) { proc_close($this->process); + $this->process = null; } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; @@ -1469,13 +1428,13 @@ private function close(): int /** * Resets data related to the latest run of the process. */ - private function resetProcessData() + private function resetProcessData(): void { $this->starttime = null; $this->callback = null; $this->exitcode = null; $this->fallbackStatus = []; - $this->processInformation = null; + $this->processInformation = []; $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->process = null; @@ -1542,8 +1501,6 @@ private function doSignal(int $signal, bool $throwException): bool private function prepareWindowsCommandLine(string $cmd, array &$env): string { $uid = uniqid('', true); - $varCount = 0; - $varCache = []; $cmd = preg_replace_callback( '/"(?:( [^"%!^]*+ @@ -1552,7 +1509,9 @@ private function prepareWindowsCommandLine(string $cmd, array &$env): string [^"%!^]*+ )++ ) | [^"]*+ )"/x', - function ($m) use (&$env, &$varCache, &$varCount, $uid) { + function ($m) use (&$env, $uid) { + static $varCount = 0; + static $varCache = []; if (!isset($m[1])) { return $m[0]; } @@ -1590,7 +1549,7 @@ function ($m) use (&$env, &$varCache, &$varCount, $uid) { * * @throws LogicException if the process has not run */ - private function requireProcessIsStarted(string $functionName) + private function requireProcessIsStarted(string $functionName): void { if (!$this->isStarted()) { throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); @@ -1602,7 +1561,7 @@ private function requireProcessIsStarted(string $functionName) * * @throws LogicException if the process is not yet terminated */ - private function requireProcessIsTerminated(string $functionName) + private function requireProcessIsTerminated(string $functionName): void { if (!$this->isTerminated()) { throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); @@ -1631,7 +1590,7 @@ private function escapeArgument(?string $argument): string return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; } - private function replacePlaceholders(string $commandline, array $env) + private function replacePlaceholders(string $commandline, array $env): string { return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { diff --git a/symfony/process/ProcessUtils.php b/symfony/process/ProcessUtils.php index 2a7aff71b..092c5ccf7 100644 --- a/symfony/process/ProcessUtils.php +++ b/symfony/process/ProcessUtils.php @@ -35,19 +35,14 @@ private function __construct() * @param string $caller The name of method call that validates the input * @param mixed $input The input to validate * - * @return mixed - * * @throws InvalidArgumentException In case the input is not valid */ - public static function validateInput(string $caller, $input) + public static function validateInput(string $caller, mixed $input): mixed { if (null !== $input) { if (\is_resource($input)) { return $input; } - if (\is_string($input)) { - return $input; - } if (\is_scalar($input)) { return (string) $input; } diff --git a/symfony/routing/Alias.php b/symfony/routing/Alias.php index f3e1d5a85..7627f12ca 100644 --- a/symfony/routing/Alias.php +++ b/symfony/routing/Alias.php @@ -15,18 +15,15 @@ class Alias { - private $id; - private $deprecation = []; + private string $id; + private array $deprecation = []; public function __construct(string $id) { $this->id = $id; } - /** - * @return static - */ - public function withId(string $id): self + public function withId(string $id): static { $new = clone $this; @@ -56,7 +53,7 @@ public function getId(): string * * @throws InvalidArgumentException when the message template is invalid */ - public function setDeprecated(string $package, string $version, string $message): self + public function setDeprecated(string $package, string $version, string $message): static { if ('' !== $message) { if (preg_match('#[\r\n]|\*/#', $message)) { diff --git a/symfony/routing/Annotation/Route.php b/symfony/routing/Annotation/Route.php index 81563df20..dda3bdade 100644 --- a/symfony/routing/Annotation/Route.php +++ b/symfony/routing/Annotation/Route.php @@ -11,262 +11,13 @@ namespace Symfony\Component\Routing\Annotation; -/** - * Annotation class for @Route(). - * - * @Annotation - * @NamedArgumentConstructor - * @Target({"CLASS", "METHOD"}) - * - * @author Fabien Potencier - * @author Alexander M. Turek - */ -#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] -class Route -{ - private $path; - private $localizedPaths = []; - private $name; - private $requirements = []; - private $options = []; - private $defaults = []; - private $host; - private $methods = []; - private $schemes = []; - private $condition; - private $priority; - private $env; - - /** - * @param array|string $data data array managed by the Doctrine Annotations library or the path - * @param array|string|null $path - * @param string[] $requirements - * @param string[]|string $methods - * @param string[]|string $schemes - * - * @throws \BadMethodCallException - */ - public function __construct( - $data = [], - $path = null, - string $name = null, - array $requirements = [], - array $options = [], - array $defaults = [], - string $host = null, - $methods = [], - $schemes = [], - string $condition = null, - int $priority = null, - string $locale = null, - string $format = null, - bool $utf8 = null, - bool $stateless = null, - string $env = null - ) { - if (\is_string($data)) { - $data = ['path' => $data]; - } elseif (!\is_array($data)) { - throw new \TypeError(sprintf('"%s": Argument $data is expected to be a string or array, got "%s".', __METHOD__, get_debug_type($data))); - } elseif ([] !== $data) { - $deprecation = false; - foreach ($data as $key => $val) { - if (\in_array($key, ['path', 'name', 'requirements', 'options', 'defaults', 'host', 'methods', 'schemes', 'condition', 'priority', 'locale', 'format', 'utf8', 'stateless', 'env', 'value'])) { - $deprecation = true; - } - } - - if ($deprecation) { - trigger_deprecation('symfony/routing', '5.3', 'Passing an array as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__); - } else { - $localizedPaths = $data; - $data = ['path' => $localizedPaths]; - } - } - if (null !== $path && !\is_string($path) && !\is_array($path)) { - throw new \TypeError(sprintf('"%s": Argument $path is expected to be a string, array or null, got "%s".', __METHOD__, get_debug_type($path))); - } - - $data['path'] = $data['path'] ?? $path; - $data['name'] = $data['name'] ?? $name; - $data['requirements'] = $data['requirements'] ?? $requirements; - $data['options'] = $data['options'] ?? $options; - $data['defaults'] = $data['defaults'] ?? $defaults; - $data['host'] = $data['host'] ?? $host; - $data['methods'] = $data['methods'] ?? $methods; - $data['schemes'] = $data['schemes'] ?? $schemes; - $data['condition'] = $data['condition'] ?? $condition; - $data['priority'] = $data['priority'] ?? $priority; - $data['locale'] = $data['locale'] ?? $locale; - $data['format'] = $data['format'] ?? $format; - $data['utf8'] = $data['utf8'] ?? $utf8; - $data['stateless'] = $data['stateless'] ?? $stateless; - $data['env'] = $data['env'] ?? $env; - - $data = array_filter($data, static function ($value): bool { - return null !== $value; - }); - - if (isset($data['localized_paths'])) { - throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', static::class)); - } - - if (isset($data['value'])) { - $data[\is_array($data['value']) ? 'localized_paths' : 'path'] = $data['value']; - unset($data['value']); - } - - if (isset($data['path']) && \is_array($data['path'])) { - $data['localized_paths'] = $data['path']; - unset($data['path']); - } - - if (isset($data['locale'])) { - $data['defaults']['_locale'] = $data['locale']; - unset($data['locale']); - } - - if (isset($data['format'])) { - $data['defaults']['_format'] = $data['format']; - unset($data['format']); - } - - if (isset($data['utf8'])) { - $data['options']['utf8'] = filter_var($data['utf8'], \FILTER_VALIDATE_BOOLEAN) ?: false; - unset($data['utf8']); - } - - if (isset($data['stateless'])) { - $data['defaults']['_stateless'] = filter_var($data['stateless'], \FILTER_VALIDATE_BOOLEAN) ?: false; - unset($data['stateless']); - } - - foreach ($data as $key => $value) { - $method = 'set'.str_replace('_', '', $key); - if (!method_exists($this, $method)) { - throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, static::class)); - } - $this->$method($value); - } - } - - public function setPath(string $path) - { - $this->path = $path; - } - - public function getPath() - { - return $this->path; - } - - public function setLocalizedPaths(array $localizedPaths) - { - $this->localizedPaths = $localizedPaths; - } - - public function getLocalizedPaths(): array - { - return $this->localizedPaths; - } - - public function setHost(string $pattern) - { - $this->host = $pattern; - } - - public function getHost() - { - return $this->host; - } - - public function setName(string $name) - { - $this->name = $name; - } - - public function getName() - { - return $this->name; - } - - public function setRequirements(array $requirements) - { - $this->requirements = $requirements; - } - - public function getRequirements() - { - return $this->requirements; - } - - public function setOptions(array $options) - { - $this->options = $options; - } - - public function getOptions() - { - return $this->options; - } - - public function setDefaults(array $defaults) - { - $this->defaults = $defaults; - } - - public function getDefaults() - { - return $this->defaults; - } - - public function setSchemes($schemes) - { - $this->schemes = \is_array($schemes) ? $schemes : [$schemes]; - } - - public function getSchemes() - { - return $this->schemes; - } - - public function setMethods($methods) - { - $this->methods = \is_array($methods) ? $methods : [$methods]; - } +// do not deprecate in 6.4/7.0, to make it easier for the ecosystem to support 6.4, 7.4 and 8.0 simultaneously - public function getMethods() - { - return $this->methods; - } - - public function setCondition(?string $condition) - { - $this->condition = $condition; - } - - public function getCondition() - { - return $this->condition; - } - - public function setPriority(int $priority): void - { - $this->priority = $priority; - } - - public function getPriority(): ?int - { - return $this->priority; - } - - public function setEnv(?string $env): void - { - $this->env = $env; - } +class_exists(\Symfony\Component\Routing\Attribute\Route::class); - public function getEnv(): ?string +if (false) { + #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] + class Route extends \Symfony\Component\Routing\Attribute\Route { - return $this->env; } } diff --git a/symfony/routing/Attribute/Route.php b/symfony/routing/Attribute/Route.php new file mode 100644 index 000000000..a1d86fe61 --- /dev/null +++ b/symfony/routing/Attribute/Route.php @@ -0,0 +1,259 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Attribute; + +/** + * Annotation class for @Route(). + * + * @Annotation + * @NamedArgumentConstructor + * @Target({"CLASS", "METHOD"}) + * + * @author Fabien Potencier + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +class Route +{ + private ?string $path = null; + private array $localizedPaths = []; + private array $methods; + private array $schemes; + + /** + * @param array $requirements + * @param string[]|string $methods + * @param string[]|string $schemes + */ + public function __construct( + string|array|null $path = null, + private ?string $name = null, + private array $requirements = [], + private array $options = [], + private array $defaults = [], + private ?string $host = null, + array|string $methods = [], + array|string $schemes = [], + private ?string $condition = null, + private ?int $priority = null, + ?string $locale = null, + ?string $format = null, + ?bool $utf8 = null, + ?bool $stateless = null, + private ?string $env = null + ) { + if (\is_array($path)) { + $this->localizedPaths = $path; + } else { + $this->path = $path; + } + $this->setMethods($methods); + $this->setSchemes($schemes); + + if (null !== $locale) { + $this->defaults['_locale'] = $locale; + } + + if (null !== $format) { + $this->defaults['_format'] = $format; + } + + if (null !== $utf8) { + $this->options['utf8'] = $utf8; + } + + if (null !== $stateless) { + $this->defaults['_stateless'] = $stateless; + } + } + + /** + * @return void + */ + public function setPath(string $path) + { + $this->path = $path; + } + + /** + * @return string|null + */ + public function getPath() + { + return $this->path; + } + + /** + * @return void + */ + public function setLocalizedPaths(array $localizedPaths) + { + $this->localizedPaths = $localizedPaths; + } + + public function getLocalizedPaths(): array + { + return $this->localizedPaths; + } + + /** + * @return void + */ + public function setHost(string $pattern) + { + $this->host = $pattern; + } + + /** + * @return string|null + */ + public function getHost() + { + return $this->host; + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * @return void + */ + public function setRequirements(array $requirements) + { + $this->requirements = $requirements; + } + + /** + * @return array + */ + public function getRequirements() + { + return $this->requirements; + } + + /** + * @return void + */ + public function setOptions(array $options) + { + $this->options = $options; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @return void + */ + public function setDefaults(array $defaults) + { + $this->defaults = $defaults; + } + + /** + * @return array + */ + public function getDefaults() + { + return $this->defaults; + } + + /** + * @return void + */ + public function setSchemes(array|string $schemes) + { + $this->schemes = (array) $schemes; + } + + /** + * @return array + */ + public function getSchemes() + { + return $this->schemes; + } + + /** + * @return void + */ + public function setMethods(array|string $methods) + { + $this->methods = (array) $methods; + } + + /** + * @return array + */ + public function getMethods() + { + return $this->methods; + } + + /** + * @return void + */ + public function setCondition(?string $condition) + { + $this->condition = $condition; + } + + /** + * @return string|null + */ + public function getCondition() + { + return $this->condition; + } + + public function setPriority(int $priority): void + { + $this->priority = $priority; + } + + public function getPriority(): ?int + { + return $this->priority; + } + + public function setEnv(?string $env): void + { + $this->env = $env; + } + + public function getEnv(): ?string + { + return $this->env; + } +} + +if (!class_exists(\Symfony\Component\Routing\Annotation\Route::class, false)) { + class_alias(Route::class, \Symfony\Component\Routing\Annotation\Route::class); +} diff --git a/symfony/routing/CompiledRoute.php b/symfony/routing/CompiledRoute.php index 1449cdb92..03215e368 100644 --- a/symfony/routing/CompiledRoute.php +++ b/symfony/routing/CompiledRoute.php @@ -18,14 +18,14 @@ */ class CompiledRoute implements \Serializable { - private $variables; - private $tokens; - private $staticPrefix; - private $regex; - private $pathVariables; - private $hostVariables; - private $hostRegex; - private $hostTokens; + private array $variables; + private array $tokens; + private string $staticPrefix; + private string $regex; + private array $pathVariables; + private array $hostVariables; + private ?string $hostRegex; + private array $hostTokens; /** * @param string $staticPrefix The static prefix of the compiled route @@ -37,7 +37,7 @@ class CompiledRoute implements \Serializable * @param array $hostVariables An array of host variables * @param array $variables An array of variables (variables defined in the path and in the host patterns) */ - public function __construct(string $staticPrefix, string $regex, array $tokens, array $pathVariables, string $hostRegex = null, array $hostTokens = [], array $hostVariables = [], array $variables = []) + public function __construct(string $staticPrefix, string $regex, array $tokens, array $pathVariables, ?string $hostRegex = null, array $hostTokens = [], array $hostVariables = [], array $variables = []) { $this->staticPrefix = $staticPrefix; $this->regex = $regex; @@ -68,7 +68,7 @@ public function __serialize(): array */ final public function serialize(): string { - return serialize($this->__serialize()); + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __unserialize(array $data): void @@ -86,87 +86,71 @@ public function __unserialize(array $data): void /** * @internal */ - final public function unserialize($serialized) + final public function unserialize(string $serialized): void { $this->__unserialize(unserialize($serialized, ['allowed_classes' => false])); } /** * Returns the static prefix. - * - * @return string */ - public function getStaticPrefix() + public function getStaticPrefix(): string { return $this->staticPrefix; } /** * Returns the regex. - * - * @return string */ - public function getRegex() + public function getRegex(): string { return $this->regex; } /** * Returns the host regex. - * - * @return string|null */ - public function getHostRegex() + public function getHostRegex(): ?string { return $this->hostRegex; } /** * Returns the tokens. - * - * @return array */ - public function getTokens() + public function getTokens(): array { return $this->tokens; } /** * Returns the host tokens. - * - * @return array */ - public function getHostTokens() + public function getHostTokens(): array { return $this->hostTokens; } /** * Returns the variables. - * - * @return array */ - public function getVariables() + public function getVariables(): array { return $this->variables; } /** * Returns the path variables. - * - * @return array */ - public function getPathVariables() + public function getPathVariables(): array { return $this->pathVariables; } /** * Returns the host variables. - * - * @return array */ - public function getHostVariables() + public function getHostVariables(): array { return $this->hostVariables; } diff --git a/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php b/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php new file mode 100644 index 000000000..619fa67f4 --- /dev/null +++ b/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Registers the expression language providers. + * + * @author Fabien Potencier + */ +class AddExpressionLanguageProvidersPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('router.default')) { + return; + } + + $definition = $container->findDefinition('router.default'); + foreach ($container->findTaggedServiceIds('routing.expression_language_provider', true) as $id => $attributes) { + $definition->addMethodCall('addExpressionLanguageProvider', [new Reference($id)]); + } + } +} diff --git a/symfony/routing/DependencyInjection/RoutingResolverPass.php b/symfony/routing/DependencyInjection/RoutingResolverPass.php index 0e9b9c893..edbecc1f0 100644 --- a/symfony/routing/DependencyInjection/RoutingResolverPass.php +++ b/symfony/routing/DependencyInjection/RoutingResolverPass.php @@ -25,28 +25,18 @@ class RoutingResolverPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; - private $resolverServiceId; - private $loaderTag; - - public function __construct(string $resolverServiceId = 'routing.resolver', string $loaderTag = 'routing.loader') - { - if (0 < \func_num_args()) { - trigger_deprecation('symfony/routing', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); - } - - $this->resolverServiceId = $resolverServiceId; - $this->loaderTag = $loaderTag; - } - + /** + * @return void + */ public function process(ContainerBuilder $container) { - if (false === $container->hasDefinition($this->resolverServiceId)) { + if (false === $container->hasDefinition('routing.resolver')) { return; } - $definition = $container->getDefinition($this->resolverServiceId); + $definition = $container->getDefinition('routing.resolver'); - foreach ($this->findAndSortTaggedServices($this->loaderTag, $container) as $id) { + foreach ($this->findAndSortTaggedServices('routing.loader', $container) as $id) { $definition->addMethodCall('addLoader', [new Reference($id)]); } } diff --git a/symfony/routing/Exception/MethodNotAllowedException.php b/symfony/routing/Exception/MethodNotAllowedException.php index 27cf2125e..c96ae9b1c 100644 --- a/symfony/routing/Exception/MethodNotAllowedException.php +++ b/symfony/routing/Exception/MethodNotAllowedException.php @@ -25,14 +25,8 @@ class MethodNotAllowedException extends \RuntimeException implements ExceptionIn /** * @param string[] $allowedMethods */ - public function __construct(array $allowedMethods, ?string $message = '', int $code = 0, \Throwable $previous = null) + public function __construct(array $allowedMethods, string $message = '', int $code = 0, ?\Throwable $previous = null) { - if (null === $message) { - trigger_deprecation('symfony/routing', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - $message = ''; - } - $this->allowedMethods = array_map('strtoupper', $allowedMethods); parent::__construct($message, $code, $previous); @@ -43,7 +37,7 @@ public function __construct(array $allowedMethods, ?string $message = '', int $c * * @return string[] */ - public function getAllowedMethods() + public function getAllowedMethods(): array { return $this->allowedMethods; } diff --git a/symfony/routing/Exception/MissingMandatoryParametersException.php b/symfony/routing/Exception/MissingMandatoryParametersException.php index 57f3a40df..72d063abb 100644 --- a/symfony/routing/Exception/MissingMandatoryParametersException.php +++ b/symfony/routing/Exception/MissingMandatoryParametersException.php @@ -19,4 +19,39 @@ */ class MissingMandatoryParametersException extends \InvalidArgumentException implements ExceptionInterface { + private string $routeName = ''; + private array $missingParameters = []; + + /** + * @param string[] $missingParameters + * @param int $code + */ + public function __construct(string $routeName = '', $missingParameters = null, $code = 0, ?\Throwable $previous = null) + { + if (\is_array($missingParameters)) { + $this->routeName = $routeName; + $this->missingParameters = $missingParameters; + $message = sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', $missingParameters), $routeName); + } else { + trigger_deprecation('symfony/routing', '6.1', 'Construction of "%s" with an exception message is deprecated, provide the route name and an array of missing parameters instead.', __CLASS__); + $message = $routeName; + $previous = $code instanceof \Throwable ? $code : null; + $code = (int) $missingParameters; + } + + parent::__construct($message, $code, $previous); + } + + /** + * @return string[] + */ + public function getMissingParameters(): array + { + return $this->missingParameters; + } + + public function getRouteName(): string + { + return $this->routeName; + } } diff --git a/symfony/routing/Generator/CompiledUrlGenerator.php b/symfony/routing/Generator/CompiledUrlGenerator.php index 8cbbf8f70..de209cdcf 100644 --- a/symfony/routing/Generator/CompiledUrlGenerator.php +++ b/symfony/routing/Generator/CompiledUrlGenerator.php @@ -20,10 +20,10 @@ */ class CompiledUrlGenerator extends UrlGenerator { - private $compiledRoutes = []; - private $defaultLocale; + private array $compiledRoutes = []; + private ?string $defaultLocale; - public function __construct(array $compiledRoutes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null) + public function __construct(array $compiledRoutes, RequestContext $context, ?LoggerInterface $logger = null, ?string $defaultLocale = null) { $this->compiledRoutes = $compiledRoutes; $this->context = $context; @@ -31,7 +31,7 @@ public function __construct(array $compiledRoutes, RequestContext $context, Logg $this->defaultLocale = $defaultLocale; } - public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH) + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { $locale = $parameters['_locale'] ?? $this->context->getParameter('_locale') diff --git a/symfony/routing/Generator/ConfigurableRequirementsInterface.php b/symfony/routing/Generator/ConfigurableRequirementsInterface.php index 568f7f775..cbbbf0459 100644 --- a/symfony/routing/Generator/ConfigurableRequirementsInterface.php +++ b/symfony/routing/Generator/ConfigurableRequirementsInterface.php @@ -40,14 +40,14 @@ interface ConfigurableRequirementsInterface /** * Enables or disables the exception on incorrect parameters. * Passing null will deactivate the requirements check completely. + * + * @return void */ public function setStrictRequirements(?bool $enabled); /** * Returns whether to throw an exception on incorrect parameters. * Null means the requirements check is deactivated completely. - * - * @return bool|null */ - public function isStrictRequirements(); + public function isStrictRequirements(): ?bool; } diff --git a/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php b/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php index 9c6740b61..1144fed5c 100644 --- a/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php +++ b/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php @@ -88,10 +88,7 @@ public function getCompiledAliases(): array return $compiledAliases; } - /** - * {@inheritdoc} - */ - public function dump(array $options = []) + public function dump(array $options = []): string { return <<routes = $routes; } - /** - * {@inheritdoc} - */ - public function getRoutes() + public function getRoutes(): RouteCollection { return $this->routes; } diff --git a/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php b/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php index d4a248a5b..d3294ce26 100644 --- a/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php +++ b/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php @@ -23,15 +23,11 @@ interface GeneratorDumperInterface /** * Dumps a set of routes to a string representation of executable code * that can then be used to generate a URL of such a route. - * - * @return string */ - public function dump(array $options = []); + public function dump(array $options = []): string; /** * Gets the routes to dump. - * - * @return RouteCollection */ - public function getRoutes(); + public function getRoutes(): RouteCollection; } diff --git a/symfony/routing/Generator/UrlGenerator.php b/symfony/routing/Generator/UrlGenerator.php index d27b00004..28f30d617 100644 --- a/symfony/routing/Generator/UrlGenerator.php +++ b/symfony/routing/Generator/UrlGenerator.php @@ -30,6 +30,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt private const QUERY_FRAGMENT_DECODED = [ // RFC 3986 explicitly allows those in the query/fragment to reference other URIs unencoded '%2F' => '/', + '%252F' => '%2F', '%3F' => '?', // reserved chars that have no special meaning for HTTP URIs in a query or fragment // this excludes esp. "&", "=" and also "+" because PHP would treat it as a space (form-encoded) @@ -51,7 +52,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt protected $logger; - private $defaultLocale; + private ?string $defaultLocale; /** * This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL. @@ -82,7 +83,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt '%7C' => '|', ]; - public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null) + public function __construct(RouteCollection $routes, RequestContext $context, ?LoggerInterface $logger = null, ?string $defaultLocale = null) { $this->routes = $routes; $this->context = $context; @@ -91,46 +92,35 @@ public function __construct(RouteCollection $routes, RequestContext $context, Lo } /** - * {@inheritdoc} + * @return void */ public function setContext(RequestContext $context) { $this->context = $context; } - /** - * {@inheritdoc} - */ - public function getContext() + public function getContext(): RequestContext { return $this->context; } /** - * {@inheritdoc} + * @return void */ public function setStrictRequirements(?bool $enabled) { $this->strictRequirements = $enabled; } - /** - * {@inheritdoc} - */ - public function isStrictRequirements() + public function isStrictRequirements(): ?bool { return $this->strictRequirements; } - /** - * {@inheritdoc} - */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH) + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { $route = null; - $locale = $parameters['_locale'] - ?? $this->context->getParameter('_locale') - ?: $this->defaultLocale; + $locale = $parameters['_locale'] ?? $this->context->getParameter('_locale') ?: $this->defaultLocale; if (null !== $locale) { do { @@ -140,7 +130,7 @@ public function generate(string $name, array $parameters = [], int $referenceTyp } while (false !== $locale = strstr($locale, '_', true)); } - if (null === $route = $route ?? $this->routes->get($name)) { + if (null === $route ??= $this->routes->get($name)) { throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); } @@ -162,20 +152,18 @@ public function generate(string $name, array $parameters = [], int $referenceTyp } /** - * @return string - * * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route * @throws InvalidParameterException When a parameter value for a placeholder is not correct because * it does not match the requirement */ - protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []) + protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []): string { $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); // all params must be given if ($diff = array_diff_key($variables, $mergedParams)) { - throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', array_keys($diff)), $name)); + throw new MissingMandatoryParametersException($name, array_keys($diff)); } $url = ''; @@ -194,9 +182,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName]])); } - if ($this->logger) { - $this->logger->error($message, ['parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]]); - } + $this->logger?->error($message, ['parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]]); return ''; } @@ -249,9 +235,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]])); } - if ($this->logger) { - $this->logger->error($message, ['parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]]); - } + $this->logger?->error($message, ['parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]]); return ''; } @@ -291,9 +275,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem } // add a query string if needed - $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, function ($a, $b) { - return $a == $b ? 0 : 1; - }); + $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { if (\is_object($v)) { @@ -342,10 +324,8 @@ protected function doGenerate(array $variables, array $defaults, array $requirem * * @param string $basePath The base path * @param string $targetPath The target path - * - * @return string */ - public static function getRelativePath(string $basePath, string $targetPath) + public static function getRelativePath(string $basePath, string $targetPath): string { if ($basePath === $targetPath) { return ''; diff --git a/symfony/routing/Generator/UrlGeneratorInterface.php b/symfony/routing/Generator/UrlGeneratorInterface.php index c6d5005f9..51210b4b7 100644 --- a/symfony/routing/Generator/UrlGeneratorInterface.php +++ b/symfony/routing/Generator/UrlGeneratorInterface.php @@ -71,12 +71,10 @@ interface UrlGeneratorInterface extends RequestContextAwareInterface * * The special parameter _fragment will be used as the document fragment suffixed to the final URL. * - * @return string - * * @throws RouteNotFoundException If the named route doesn't exist * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route * @throws InvalidParameterException When a parameter value for a placeholder is not correct because * it does not match the requirement */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH); + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string; } diff --git a/symfony/routing/Loader/AnnotationClassLoader.php b/symfony/routing/Loader/AnnotationClassLoader.php index ad5af5c94..b2c52ce9e 100644 --- a/symfony/routing/Loader/AnnotationClassLoader.php +++ b/symfony/routing/Loader/AnnotationClassLoader.php @@ -11,384 +11,15 @@ namespace Symfony\Component\Routing\Loader; -use Doctrine\Common\Annotations\Reader; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Loader\LoaderResolverInterface; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Routing\Annotation\Route as RouteAnnotation; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; +trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationClassLoader::class, AttributeClassLoader::class); -/** - * AnnotationClassLoader loads routing information from a PHP class and its methods. - * - * You need to define an implementation for the configureRoute() method. Most of the - * time, this method should define some PHP callable to be called for the route - * (a controller in MVC speak). - * - * The @Route annotation can be set on the class (for global parameters), - * and on each method. - * - * The @Route annotation main value is the route path. The annotation also - * recognizes several parameters: requirements, options, defaults, schemes, - * methods, host, and name. The name parameter is mandatory. - * Here is an example of how you should be able to use it: - * /** - * * @Route("/Blog") - * * / - * class Blog - * { - * /** - * * @Route("/", name="blog_index") - * * / - * public function index() - * { - * } - * /** - * * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"}) - * * / - * public function show() - * { - * } - * } - * - * On PHP 8, the annotation class can be used as an attribute as well: - * #[Route('/Blog')] - * class Blog - * { - * #[Route('/', name: 'blog_index')] - * public function index() - * { - * } - * #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])] - * public function show() - * { - * } - * } - - * - * @author Fabien Potencier - * @author Alexander M. Turek - */ -abstract class AnnotationClassLoader implements LoaderInterface -{ - protected $reader; - protected $env; - - /** - * @var string - */ - protected $routeAnnotationClass = RouteAnnotation::class; - - /** - * @var int - */ - protected $defaultRouteIndex = 0; - - public function __construct(Reader $reader = null, string $env = null) - { - $this->reader = $reader; - $this->env = $env; - } - - /** - * Sets the annotation class to read route properties from. - */ - public function setRouteAnnotationClass(string $class) - { - $this->routeAnnotationClass = $class; - } - - /** - * Loads from annotations from a class. - * - * @param string $class A class name - * - * @return RouteCollection - * - * @throws \InvalidArgumentException When route can't be parsed - */ - public function load($class, string $type = null) - { - if (!class_exists($class)) { - throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); - } - - $class = new \ReflectionClass($class); - if ($class->isAbstract()) { - throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName())); - } - - $globals = $this->getGlobals($class); - - $collection = new RouteCollection(); - $collection->addResource(new FileResource($class->getFileName())); - - if ($globals['env'] && $this->env !== $globals['env']) { - return $collection; - } - - foreach ($class->getMethods() as $method) { - $this->defaultRouteIndex = 0; - foreach ($this->getAnnotations($method) as $annot) { - $this->addRoute($collection, $annot, $globals, $class, $method); - } - } - - if (0 === $collection->count() && $class->hasMethod('__invoke')) { - $globals = $this->resetGlobals(); - foreach ($this->getAnnotations($class) as $annot) { - $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); - } - } - - return $collection; - } - - /** - * @param RouteAnnotation $annot or an object that exposes a similar interface - */ - protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method) - { - if ($annot->getEnv() && $annot->getEnv() !== $this->env) { - return; - } - - $name = $annot->getName(); - if (null === $name) { - $name = $this->getDefaultRouteName($class, $method); - } - $name = $globals['name'].$name; - - $requirements = $annot->getRequirements(); - - foreach ($requirements as $placeholder => $requirement) { - if (\is_int($placeholder)) { - throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?', $placeholder, $requirement, $name, $class->getName(), $method->getName())); - } - } - - $defaults = array_replace($globals['defaults'], $annot->getDefaults()); - $requirements = array_replace($globals['requirements'], $requirements); - $options = array_replace($globals['options'], $annot->getOptions()); - $schemes = array_merge($globals['schemes'], $annot->getSchemes()); - $methods = array_merge($globals['methods'], $annot->getMethods()); - - $host = $annot->getHost(); - if (null === $host) { - $host = $globals['host']; - } - - $condition = $annot->getCondition() ?? $globals['condition']; - $priority = $annot->getPriority() ?? $globals['priority']; - - $path = $annot->getLocalizedPaths() ?: $annot->getPath(); - $prefix = $globals['localized_paths'] ?: $globals['path']; - $paths = []; - - if (\is_array($path)) { - if (!\is_array($prefix)) { - foreach ($path as $locale => $localePath) { - $paths[$locale] = $prefix.$localePath; - } - } elseif ($missing = array_diff_key($prefix, $path)) { - throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing)))); - } else { - foreach ($path as $locale => $localePath) { - if (!isset($prefix[$locale])) { - throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name)); - } - - $paths[$locale] = $prefix[$locale].$localePath; - } - } - } elseif (\is_array($prefix)) { - foreach ($prefix as $locale => $localePrefix) { - $paths[$locale] = $localePrefix.$path; - } - } else { - $paths[] = $prefix.$path; - } - - foreach ($method->getParameters() as $param) { - if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) { - continue; - } - foreach ($paths as $locale => $path) { - if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/', preg_quote($param->name)), $path)) { - $defaults[$param->name] = $param->getDefaultValue(); - break; - } - } - } - - foreach ($paths as $locale => $path) { - $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); - $this->configureRoute($route, $class, $method, $annot); - if (0 !== $locale) { - $route->setDefault('_locale', $locale); - $route->setRequirement('_locale', preg_quote($locale)); - $route->setDefault('_canonical_route', $name); - $collection->add($name.'.'.$locale, $route, $priority); - } else { - $collection->add($name, $route, $priority); - } - } - } +class_exists(AttributeClassLoader::class); +if (false) { /** - * {@inheritdoc} + * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeClassLoader} instead */ - public function supports($resource, string $type = null) + abstract class AnnotationClassLoader extends AttributeClassLoader { - return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type); - } - - /** - * {@inheritdoc} - */ - public function setResolver(LoaderResolverInterface $resolver) - { - } - - /** - * {@inheritdoc} - */ - public function getResolver() - { - } - - /** - * Gets the default route name for a class method. - * - * @return string - */ - protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) - { - $name = str_replace('\\', '_', $class->name).'_'.$method->name; - $name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name); - if ($this->defaultRouteIndex > 0) { - $name .= '_'.$this->defaultRouteIndex; - } - ++$this->defaultRouteIndex; - - return $name; - } - - protected function getGlobals(\ReflectionClass $class) - { - $globals = $this->resetGlobals(); - - $annot = null; - if (\PHP_VERSION_ID >= 80000 && ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)) { - $annot = $attribute->newInstance(); - } - if (!$annot && $this->reader) { - $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass); - } - - if ($annot) { - if (null !== $annot->getName()) { - $globals['name'] = $annot->getName(); - } - - if (null !== $annot->getPath()) { - $globals['path'] = $annot->getPath(); - } - - $globals['localized_paths'] = $annot->getLocalizedPaths(); - - if (null !== $annot->getRequirements()) { - $globals['requirements'] = $annot->getRequirements(); - } - - if (null !== $annot->getOptions()) { - $globals['options'] = $annot->getOptions(); - } - - if (null !== $annot->getDefaults()) { - $globals['defaults'] = $annot->getDefaults(); - } - - if (null !== $annot->getSchemes()) { - $globals['schemes'] = $annot->getSchemes(); - } - - if (null !== $annot->getMethods()) { - $globals['methods'] = $annot->getMethods(); - } - - if (null !== $annot->getHost()) { - $globals['host'] = $annot->getHost(); - } - - if (null !== $annot->getCondition()) { - $globals['condition'] = $annot->getCondition(); - } - - $globals['priority'] = $annot->getPriority() ?? 0; - $globals['env'] = $annot->getEnv(); - - foreach ($globals['requirements'] as $placeholder => $requirement) { - if (\is_int($placeholder)) { - throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?', $placeholder, $requirement, $class->getName())); - } - } - } - - return $globals; - } - - private function resetGlobals(): array - { - return [ - 'path' => null, - 'localized_paths' => [], - 'requirements' => [], - 'options' => [], - 'defaults' => [], - 'schemes' => [], - 'methods' => [], - 'host' => '', - 'condition' => '', - 'name' => '', - 'priority' => 0, - 'env' => null, - ]; - } - - protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition) - { - return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); - } - - abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot); - - /** - * @param \ReflectionClass|\ReflectionMethod $reflection - * - * @return iterable - */ - private function getAnnotations(object $reflection): iterable - { - if (\PHP_VERSION_ID >= 80000) { - foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - yield $attribute->newInstance(); - } - } - - if (!$this->reader) { - return; - } - - $anntotations = $reflection instanceof \ReflectionClass - ? $this->reader->getClassAnnotations($reflection) - : $this->reader->getMethodAnnotations($reflection); - - foreach ($anntotations as $annotation) { - if ($annotation instanceof $this->routeAnnotationClass) { - yield $annotation; - } - } } } diff --git a/symfony/routing/Loader/AnnotationDirectoryLoader.php b/symfony/routing/Loader/AnnotationDirectoryLoader.php index ae825a39f..169b1e60a 100644 --- a/symfony/routing/Loader/AnnotationDirectoryLoader.php +++ b/symfony/routing/Loader/AnnotationDirectoryLoader.php @@ -11,83 +11,15 @@ namespace Symfony\Component\Routing\Loader; -use Symfony\Component\Config\Resource\DirectoryResource; -use Symfony\Component\Routing\RouteCollection; +trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationDirectoryLoader::class, AttributeDirectoryLoader::class); -/** - * AnnotationDirectoryLoader loads routing information from annotations set - * on PHP classes and methods. - * - * @author Fabien Potencier - */ -class AnnotationDirectoryLoader extends AnnotationFileLoader -{ - /** - * Loads from annotations from a directory. - * - * @param string $path A directory path - * @param string|null $type The resource type - * - * @return RouteCollection - * - * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed - */ - public function load($path, string $type = null) - { - if (!is_dir($dir = $this->locator->locate($path))) { - return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection(); - } - - $collection = new RouteCollection(); - $collection->addResource(new DirectoryResource($dir, '/\.php$/')); - $files = iterator_to_array(new \RecursiveIteratorIterator( - new \RecursiveCallbackFilterIterator( - new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), - function (\SplFileInfo $current) { - return '.' !== substr($current->getBasename(), 0, 1); - } - ), - \RecursiveIteratorIterator::LEAVES_ONLY - )); - usort($files, function (\SplFileInfo $a, \SplFileInfo $b) { - return (string) $a > (string) $b ? 1 : -1; - }); - - foreach ($files as $file) { - if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { - continue; - } - - if ($class = $this->findClass($file)) { - $refl = new \ReflectionClass($class); - if ($refl->isAbstract()) { - continue; - } - - $collection->addCollection($this->loader->load($class, $type)); - } - } - - return $collection; - } +class_exists(AttributeDirectoryLoader::class); +if (false) { /** - * {@inheritdoc} + * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeDirectoryLoader} instead */ - public function supports($resource, string $type = null) + class AnnotationDirectoryLoader extends AttributeDirectoryLoader { - if ('annotation' === $type) { - return true; - } - - if ($type || !\is_string($resource)) { - return false; - } - - try { - return is_dir($this->locator->locate($resource)); - } catch (\Exception $e) { - return false; - } } } diff --git a/symfony/routing/Loader/AnnotationFileLoader.php b/symfony/routing/Loader/AnnotationFileLoader.php index 27af66ee6..60487bb27 100644 --- a/symfony/routing/Loader/AnnotationFileLoader.php +++ b/symfony/routing/Loader/AnnotationFileLoader.php @@ -11,136 +11,15 @@ namespace Symfony\Component\Routing\Loader; -use Symfony\Component\Config\FileLocatorInterface; -use Symfony\Component\Config\Loader\FileLoader; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Routing\RouteCollection; +trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationFileLoader::class, AttributeFileLoader::class); -/** - * AnnotationFileLoader loads routing information from annotations set - * on a PHP class and its methods. - * - * @author Fabien Potencier - */ -class AnnotationFileLoader extends FileLoader -{ - protected $loader; - - public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader) - { - if (!\function_exists('token_get_all')) { - throw new \LogicException('The Tokenizer extension is required for the routing annotation loaders.'); - } - - parent::__construct($locator); - - $this->loader = $loader; - } - - /** - * Loads from annotations from a file. - * - * @param string $file A PHP file path - * @param string|null $type The resource type - * - * @return RouteCollection|null - * - * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed - */ - public function load($file, string $type = null) - { - $path = $this->locator->locate($file); - - $collection = new RouteCollection(); - if ($class = $this->findClass($path)) { - $refl = new \ReflectionClass($class); - if ($refl->isAbstract()) { - return null; - } - - $collection->addResource(new FileResource($path)); - $collection->addCollection($this->loader->load($class, $type)); - } - - gc_mem_caches(); - - return $collection; - } - - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) - { - return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'annotation' === $type); - } +class_exists(AttributeFileLoader::class); +if (false) { /** - * Returns the full class name for the first class in the file. - * - * @return string|false + * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeFileLoader} instead */ - protected function findClass(string $file) + class AnnotationFileLoader extends AttributeFileLoader { - $class = false; - $namespace = false; - $tokens = token_get_all(file_get_contents($file)); - - if (1 === \count($tokens) && \T_INLINE_HTML === $tokens[0][0]) { - throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forgot to add the " true, \T_STRING => true]; - if (\defined('T_NAME_QUALIFIED')) { - $nsTokens[\T_NAME_QUALIFIED] = true; - } - for ($i = 0; isset($tokens[$i]); ++$i) { - $token = $tokens[$i]; - if (!isset($token[1])) { - continue; - } - - if (true === $class && \T_STRING === $token[0]) { - return $namespace.'\\'.$token[1]; - } - - if (true === $namespace && isset($nsTokens[$token[0]])) { - $namespace = $token[1]; - while (isset($tokens[++$i][1], $nsTokens[$tokens[$i][0]])) { - $namespace .= $tokens[$i][1]; - } - $token = $tokens[$i]; - } - - if (\T_CLASS === $token[0]) { - // Skip usage of ::class constant and anonymous classes - $skipClassToken = false; - for ($j = $i - 1; $j > 0; --$j) { - if (!isset($tokens[$j][1])) { - if ('(' === $tokens[$j] || ',' === $tokens[$j]) { - $skipClassToken = true; - } - break; - } - - if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { - $skipClassToken = true; - break; - } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { - break; - } - } - - if (!$skipClassToken) { - $class = true; - } - } - - if (\T_NAMESPACE === $token[0]) { - $namespace = true; - } - } - - return false; } } diff --git a/symfony/routing/Loader/AttributeClassLoader.php b/symfony/routing/Loader/AttributeClassLoader.php new file mode 100644 index 000000000..132da8028 --- /dev/null +++ b/symfony/routing/Loader/AttributeClassLoader.php @@ -0,0 +1,431 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Config\Loader\LoaderResolverInterface; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Attribute\Route as RouteAnnotation; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * AttributeClassLoader loads routing information from a PHP class and its methods. + * + * You need to define an implementation for the configureRoute() method. Most of the + * time, this method should define some PHP callable to be called for the route + * (a controller in MVC speak). + * + * The #[Route] attribute can be set on the class (for global parameters), + * and on each method. + * + * The #[Route] attribute main value is the route path. The attribute also + * recognizes several parameters: requirements, options, defaults, schemes, + * methods, host, and name. The name parameter is mandatory. + * Here is an example of how you should be able to use it: + * + * #[Route('/Blog')] + * class Blog + * { + * #[Route('/', name: 'blog_index')] + * public function index() + * { + * } + * #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])] + * public function show() + * { + * } + * } + * + * @author Fabien Potencier + * @author Alexander M. Turek + * @author Alexandre Daubois + */ +abstract class AttributeClassLoader implements LoaderInterface +{ + /** + * @var Reader|null + * + * @deprecated in Symfony 6.4, this property will be removed in Symfony 7. + */ + protected $reader; + + /** + * @var string|null + */ + protected $env; + + /** + * @var string + */ + protected $routeAnnotationClass = RouteAnnotation::class; + + /** + * @var int + */ + protected $defaultRouteIndex = 0; + + private bool $hasDeprecatedAnnotations = false; + + /** + * @param string|null $env + */ + public function __construct($env = null) + { + if ($env instanceof Reader || null === $env && \func_num_args() > 1 && null !== func_get_arg(1)) { + trigger_deprecation('symfony/routing', '6.4', 'Passing an instance of "%s" as first and the environment as second argument to "%s" is deprecated. Pass the environment as first argument instead.', Reader::class, __METHOD__); + + $this->reader = $env; + $env = \func_num_args() > 1 ? func_get_arg(1) : null; + } + + if (\is_string($env) || null === $env) { + $this->env = $env; + } elseif ($env instanceof \Stringable || \is_scalar($env)) { + $this->env = (string) $env; + } else { + throw new \TypeError(__METHOD__.sprintf(': Parameter $env was expected to be a string or null, "%s" given.', get_debug_type($env))); + } + } + + /** + * Sets the annotation class to read route properties from. + * + * @return void + */ + public function setRouteAnnotationClass(string $class) + { + $this->routeAnnotationClass = $class; + } + + /** + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load(mixed $class, ?string $type = null): RouteCollection + { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + $class = new \ReflectionClass($class); + if ($class->isAbstract()) { + throw new \InvalidArgumentException(sprintf('Attributes from class "%s" cannot be read as it is abstract.', $class->getName())); + } + + $this->hasDeprecatedAnnotations = false; + + try { + $globals = $this->getGlobals($class); + $collection = new RouteCollection(); + $collection->addResource(new FileResource($class->getFileName())); + if ($globals['env'] && $this->env !== $globals['env']) { + return $collection; + } + $fqcnAlias = false; + foreach ($class->getMethods() as $method) { + $this->defaultRouteIndex = 0; + $routeNamesBefore = array_keys($collection->all()); + foreach ($this->getAnnotations($method) as $annot) { + $this->addRoute($collection, $annot, $globals, $class, $method); + if ('__invoke' === $method->name) { + $fqcnAlias = true; + } + } + + if (1 === $collection->count() - \count($routeNamesBefore)) { + $newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore)); + if ($newRouteName !== $aliasName = sprintf('%s::%s', $class->name, $method->name)) { + $collection->addAlias($aliasName, $newRouteName); + } + } + } + if (0 === $collection->count() && $class->hasMethod('__invoke')) { + $globals = $this->resetGlobals(); + foreach ($this->getAnnotations($class) as $annot) { + $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); + $fqcnAlias = true; + } + } + if ($fqcnAlias && 1 === $collection->count()) { + $invokeRouteName = key($collection->all()); + if ($invokeRouteName !== $class->name) { + $collection->addAlias($class->name, $invokeRouteName); + } + + if ($invokeRouteName !== $aliasName = sprintf('%s::__invoke', $class->name)) { + $collection->addAlias($aliasName, $invokeRouteName); + } + } + + if ($this->hasDeprecatedAnnotations) { + trigger_deprecation('symfony/routing', '6.4', 'Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.', $class->getName()); + } + } finally { + $this->hasDeprecatedAnnotations = false; + } + + return $collection; + } + + /** + * @param RouteAnnotation $annot or an object that exposes a similar interface + * + * @return void + */ + protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method) + { + if ($annot->getEnv() && $annot->getEnv() !== $this->env) { + return; + } + + $name = $annot->getName() ?? $this->getDefaultRouteName($class, $method); + $name = $globals['name'].$name; + + $requirements = $annot->getRequirements(); + + foreach ($requirements as $placeholder => $requirement) { + if (\is_int($placeholder)) { + throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?', $placeholder, $requirement, $name, $class->getName(), $method->getName())); + } + } + + $defaults = array_replace($globals['defaults'], $annot->getDefaults()); + $requirements = array_replace($globals['requirements'], $requirements); + $options = array_replace($globals['options'], $annot->getOptions()); + $schemes = array_unique(array_merge($globals['schemes'], $annot->getSchemes())); + $methods = array_unique(array_merge($globals['methods'], $annot->getMethods())); + + $host = $annot->getHost() ?? $globals['host']; + $condition = $annot->getCondition() ?? $globals['condition']; + $priority = $annot->getPriority() ?? $globals['priority']; + + $path = $annot->getLocalizedPaths() ?: $annot->getPath(); + $prefix = $globals['localized_paths'] ?: $globals['path']; + $paths = []; + + if (\is_array($path)) { + if (!\is_array($prefix)) { + foreach ($path as $locale => $localePath) { + $paths[$locale] = $prefix.$localePath; + } + } elseif ($missing = array_diff_key($prefix, $path)) { + throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing)))); + } else { + foreach ($path as $locale => $localePath) { + if (!isset($prefix[$locale])) { + throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name)); + } + + $paths[$locale] = $prefix[$locale].$localePath; + } + } + } elseif (\is_array($prefix)) { + foreach ($prefix as $locale => $localePrefix) { + $paths[$locale] = $localePrefix.$path; + } + } else { + $paths[] = $prefix.$path; + } + + foreach ($method->getParameters() as $param) { + if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) { + continue; + } + foreach ($paths as $locale => $path) { + if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/', preg_quote($param->name)), $path)) { + if (\is_scalar($defaultValue = $param->getDefaultValue()) || null === $defaultValue) { + $defaults[$param->name] = $defaultValue; + } elseif ($defaultValue instanceof \BackedEnum) { + $defaults[$param->name] = $defaultValue->value; + } + break; + } + } + } + + foreach ($paths as $locale => $path) { + $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $this->configureRoute($route, $class, $method, $annot); + if (0 !== $locale) { + $route->setDefault('_locale', $locale); + $route->setRequirement('_locale', preg_quote($locale)); + $route->setDefault('_canonical_route', $name); + $collection->add($name.'.'.$locale, $route, $priority); + } else { + $collection->add($name, $route, $priority); + } + } + } + + public function supports(mixed $resource, ?string $type = null): bool + { + if ('annotation' === $type) { + trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.'); + } + + return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || \in_array($type, ['annotation', 'attribute'], true)); + } + + public function setResolver(LoaderResolverInterface $resolver): void + { + } + + public function getResolver(): LoaderResolverInterface + { + } + + /** + * Gets the default route name for a class method. + * + * @return string + */ + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + { + $name = str_replace('\\', '_', $class->name).'_'.$method->name; + $name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name); + if ($this->defaultRouteIndex > 0) { + $name .= '_'.$this->defaultRouteIndex; + } + ++$this->defaultRouteIndex; + + return $name; + } + + /** + * @return array + */ + protected function getGlobals(\ReflectionClass $class) + { + $globals = $this->resetGlobals(); + + $annot = null; + if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + $annot = $attribute->newInstance(); + } + if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) { + $this->hasDeprecatedAnnotations = true; + } + + if ($annot) { + if (null !== $annot->getName()) { + $globals['name'] = $annot->getName(); + } + + if (null !== $annot->getPath()) { + $globals['path'] = $annot->getPath(); + } + + $globals['localized_paths'] = $annot->getLocalizedPaths(); + + if (null !== $annot->getRequirements()) { + $globals['requirements'] = $annot->getRequirements(); + } + + if (null !== $annot->getOptions()) { + $globals['options'] = $annot->getOptions(); + } + + if (null !== $annot->getDefaults()) { + $globals['defaults'] = $annot->getDefaults(); + } + + if (null !== $annot->getSchemes()) { + $globals['schemes'] = $annot->getSchemes(); + } + + if (null !== $annot->getMethods()) { + $globals['methods'] = $annot->getMethods(); + } + + if (null !== $annot->getHost()) { + $globals['host'] = $annot->getHost(); + } + + if (null !== $annot->getCondition()) { + $globals['condition'] = $annot->getCondition(); + } + + $globals['priority'] = $annot->getPriority() ?? 0; + $globals['env'] = $annot->getEnv(); + + foreach ($globals['requirements'] as $placeholder => $requirement) { + if (\is_int($placeholder)) { + throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?', $placeholder, $requirement, $class->getName())); + } + } + } + + return $globals; + } + + private function resetGlobals(): array + { + return [ + 'path' => null, + 'localized_paths' => [], + 'requirements' => [], + 'options' => [], + 'defaults' => [], + 'schemes' => [], + 'methods' => [], + 'host' => '', + 'condition' => '', + 'name' => '', + 'priority' => 0, + 'env' => null, + ]; + } + + /** + * @return Route + */ + protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition) + { + return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + } + + /** + * @return void + */ + abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot); + + /** + * @return iterable + */ + private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): iterable + { + foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + yield $attribute->newInstance(); + } + + if (!$this->reader) { + return; + } + + $annotations = $reflection instanceof \ReflectionClass + ? $this->reader->getClassAnnotations($reflection) + : $this->reader->getMethodAnnotations($reflection); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $this->routeAnnotationClass) { + $this->hasDeprecatedAnnotations = true; + + yield $annotation; + } + } + } +} + +if (!class_exists(AnnotationClassLoader::class, false)) { + class_alias(AttributeClassLoader::class, AnnotationClassLoader::class); +} diff --git a/symfony/routing/Loader/AttributeDirectoryLoader.php b/symfony/routing/Loader/AttributeDirectoryLoader.php new file mode 100644 index 000000000..a070937d3 --- /dev/null +++ b/symfony/routing/Loader/AttributeDirectoryLoader.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * AttributeDirectoryLoader loads routing information from attributes set + * on PHP classes and methods. + * + * @author Fabien Potencier + * @author Alexandre Daubois + */ +class AttributeDirectoryLoader extends AttributeFileLoader +{ + /** + * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed + */ + public function load(mixed $path, ?string $type = null): ?RouteCollection + { + if (!is_dir($dir = $this->locator->locate($path))) { + return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection(); + } + + $collection = new RouteCollection(); + $collection->addResource(new DirectoryResource($dir, '/\.php$/')); + $files = iterator_to_array(new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + fn (\SplFileInfo $current) => !str_starts_with($current->getBasename(), '.') + ), + \RecursiveIteratorIterator::LEAVES_ONLY + )); + usort($files, fn (\SplFileInfo $a, \SplFileInfo $b) => (string) $a > (string) $b ? 1 : -1); + + foreach ($files as $file) { + if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { + continue; + } + + if ($class = $this->findClass($file)) { + $refl = new \ReflectionClass($class); + if ($refl->isAbstract()) { + continue; + } + + $collection->addCollection($this->loader->load($class, $type)); + } + } + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + if (!\is_string($resource)) { + return false; + } + + if (\in_array($type, ['annotation', 'attribute'], true)) { + if ('annotation' === $type) { + trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.'); + } + + return true; + } + + if ($type) { + return false; + } + + try { + return is_dir($this->locator->locate($resource)); + } catch (\Exception) { + return false; + } + } +} + +if (!class_exists(AnnotationDirectoryLoader::class, false)) { + class_alias(AttributeDirectoryLoader::class, AnnotationDirectoryLoader::class); +} diff --git a/symfony/routing/Loader/AttributeFileLoader.php b/symfony/routing/Loader/AttributeFileLoader.php new file mode 100644 index 000000000..e9a13e597 --- /dev/null +++ b/symfony/routing/Loader/AttributeFileLoader.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * AttributeFileLoader loads routing information from attributes set + * on a PHP class and its methods. + * + * @author Fabien Potencier + * @author Alexandre Daubois + */ +class AttributeFileLoader extends FileLoader +{ + protected $loader; + + public function __construct(FileLocatorInterface $locator, AttributeClassLoader $loader) + { + if (!\function_exists('token_get_all')) { + throw new \LogicException('The Tokenizer extension is required for the routing attribute loader.'); + } + + parent::__construct($locator); + + $this->loader = $loader; + } + + /** + * Loads from attributes from a file. + * + * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed + */ + public function load(mixed $file, ?string $type = null): ?RouteCollection + { + $path = $this->locator->locate($file); + + $collection = new RouteCollection(); + if ($class = $this->findClass($path)) { + $refl = new \ReflectionClass($class); + if ($refl->isAbstract()) { + return null; + } + + $collection->addResource(new FileResource($path)); + $collection->addCollection($this->loader->load($class, $type)); + } + + gc_mem_caches(); + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + if ('annotation' === $type) { + trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.'); + } + + return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || \in_array($type, ['annotation', 'attribute'], true)); + } + + /** + * Returns the full class name for the first class in the file. + */ + protected function findClass(string $file): string|false + { + $class = false; + $namespace = false; + $tokens = token_get_all(file_get_contents($file)); + + if (1 === \count($tokens) && \T_INLINE_HTML === $tokens[0][0]) { + throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forget to add the " true, \T_STRING => true]; + if (\defined('T_NAME_QUALIFIED')) { + $nsTokens[\T_NAME_QUALIFIED] = true; + } + for ($i = 0; isset($tokens[$i]); ++$i) { + $token = $tokens[$i]; + if (!isset($token[1])) { + continue; + } + + if (true === $class && \T_STRING === $token[0]) { + return $namespace.'\\'.$token[1]; + } + + if (true === $namespace && isset($nsTokens[$token[0]])) { + $namespace = $token[1]; + while (isset($tokens[++$i][1], $nsTokens[$tokens[$i][0]])) { + $namespace .= $tokens[$i][1]; + } + $token = $tokens[$i]; + } + + if (\T_CLASS === $token[0]) { + // Skip usage of ::class constant and anonymous classes + $skipClassToken = false; + for ($j = $i - 1; $j > 0; --$j) { + if (!isset($tokens[$j][1])) { + if ('(' === $tokens[$j] || ',' === $tokens[$j]) { + $skipClassToken = true; + } + break; + } + + if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { + $skipClassToken = true; + break; + } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { + break; + } + } + + if (!$skipClassToken) { + $class = true; + } + } + + if (\T_NAMESPACE === $token[0]) { + $namespace = true; + } + } + + return false; + } +} + +if (!class_exists(AnnotationFileLoader::class, false)) { + class_alias(AttributeFileLoader::class, AnnotationFileLoader::class); +} diff --git a/symfony/routing/Loader/ClosureLoader.php b/symfony/routing/Loader/ClosureLoader.php index 42f950f50..dcc5ee333 100644 --- a/symfony/routing/Loader/ClosureLoader.php +++ b/symfony/routing/Loader/ClosureLoader.php @@ -25,21 +25,13 @@ class ClosureLoader extends Loader { /** * Loads a Closure. - * - * @param \Closure $closure A Closure - * @param string|null $type The resource type - * - * @return RouteCollection */ - public function load($closure, string $type = null) + public function load(mixed $closure, ?string $type = null): RouteCollection { return $closure($this->env); } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { return $resource instanceof \Closure && (!$type || 'closure' === $type); } diff --git a/symfony/routing/Loader/Configurator/AliasConfigurator.php b/symfony/routing/Loader/Configurator/AliasConfigurator.php index 4b2206e68..c908456e4 100644 --- a/symfony/routing/Loader/Configurator/AliasConfigurator.php +++ b/symfony/routing/Loader/Configurator/AliasConfigurator.php @@ -16,7 +16,7 @@ class AliasConfigurator { - private $alias; + private Alias $alias; public function __construct(Alias $alias) { @@ -34,7 +34,7 @@ public function __construct(Alias $alias) * * @throws InvalidArgumentException when the message template is invalid */ - public function deprecate(string $package, string $version, string $message): self + public function deprecate(string $package, string $version, string $message): static { $this->alias->setDeprecated($package, $version, $message); diff --git a/symfony/routing/Loader/Configurator/CollectionConfigurator.php b/symfony/routing/Loader/Configurator/CollectionConfigurator.php index 09274ccdc..1abf3bc0c 100644 --- a/symfony/routing/Loader/Configurator/CollectionConfigurator.php +++ b/symfony/routing/Loader/Configurator/CollectionConfigurator.php @@ -23,12 +23,12 @@ class CollectionConfigurator use Traits\HostTrait; use Traits\RouteTrait; - private $parent; - private $parentConfigurator; - private $parentPrefixes; - private $host; + private RouteCollection $parent; + private ?CollectionConfigurator $parentConfigurator; + private ?array $parentPrefixes; + private string|array|null $host = null; - public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null, array $parentPrefixes = null) + public function __construct(RouteCollection $parent, string $name, ?self $parentConfigurator = null, ?array $parentPrefixes = null) { $this->parent = $parent; $this->name = $name; @@ -38,14 +38,14 @@ public function __construct(RouteCollection $parent, string $name, self $parentC $this->parentPrefixes = $parentPrefixes; } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } + /** + * @return void + */ public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); @@ -78,7 +78,7 @@ final public function collection(string $name = ''): self * * @return $this */ - final public function prefix($prefix): self + final public function prefix(string|array $prefix): static { if (\is_array($prefix)) { if (null === $this->parentPrefixes) { @@ -111,13 +111,16 @@ final public function prefix($prefix): self * * @return $this */ - final public function host($host): self + final public function host(string|array $host): static { $this->host = $host; return $this; } + /** + * This method overrides the one from LocalizedRouteTrait. + */ private function createRoute(string $path): Route { return (clone $this->route)->setPath($path); diff --git a/symfony/routing/Loader/Configurator/ImportConfigurator.php b/symfony/routing/Loader/Configurator/ImportConfigurator.php index 32f3efe3a..9c92a7d72 100644 --- a/symfony/routing/Loader/Configurator/ImportConfigurator.php +++ b/symfony/routing/Loader/Configurator/ImportConfigurator.php @@ -22,7 +22,7 @@ class ImportConfigurator use Traits\PrefixTrait; use Traits\RouteTrait; - private $parent; + private RouteCollection $parent; public function __construct(RouteCollection $parent, RouteCollection $route) { @@ -30,14 +30,14 @@ public function __construct(RouteCollection $parent, RouteCollection $route) $this->route = $route; } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } + /** + * @return void + */ public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); @@ -55,7 +55,7 @@ public function __destruct() * * @return $this */ - final public function prefix($prefix, bool $trailingSlashOnRoot = true): self + final public function prefix(string|array $prefix, bool $trailingSlashOnRoot = true): static { $this->addPrefix($this->route, $prefix, $trailingSlashOnRoot); @@ -67,7 +67,7 @@ final public function prefix($prefix, bool $trailingSlashOnRoot = true): self * * @return $this */ - final public function namePrefix(string $namePrefix): self + final public function namePrefix(string $namePrefix): static { $this->route->addNamePrefix($namePrefix); @@ -81,7 +81,7 @@ final public function namePrefix(string $namePrefix): self * * @return $this */ - final public function host($host): self + final public function host(string|array $host): static { $this->addHost($this->route, $host); diff --git a/symfony/routing/Loader/Configurator/RouteConfigurator.php b/symfony/routing/Loader/Configurator/RouteConfigurator.php index bb6ce267a..d9d441da1 100644 --- a/symfony/routing/Loader/Configurator/RouteConfigurator.php +++ b/symfony/routing/Loader/Configurator/RouteConfigurator.php @@ -24,7 +24,7 @@ class RouteConfigurator protected $parentConfigurator; - public function __construct(RouteCollection $collection, RouteCollection $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null) + public function __construct(RouteCollection $collection, RouteCollection $route, string $name = '', ?CollectionConfigurator $parentConfigurator = null, ?array $prefixes = null) { $this->collection = $collection; $this->route = $route; @@ -40,7 +40,7 @@ public function __construct(RouteCollection $collection, RouteCollection $route, * * @return $this */ - final public function host($host): self + final public function host(string|array $host): static { $this->addHost($this->route, $host); diff --git a/symfony/routing/Loader/Configurator/RoutingConfigurator.php b/symfony/routing/Loader/Configurator/RoutingConfigurator.php index 4687bf681..fa88aa677 100644 --- a/symfony/routing/Loader/Configurator/RoutingConfigurator.php +++ b/symfony/routing/Loader/Configurator/RoutingConfigurator.php @@ -21,12 +21,12 @@ class RoutingConfigurator { use Traits\AddTrait; - private $loader; - private $path; - private $file; - private $env; + private PhpFileLoader $loader; + private string $path; + private string $file; + private ?string $env; - public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, string $env = null) + public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, ?string $env = null) { $this->collection = $collection; $this->loader = $loader; @@ -38,7 +38,7 @@ public function __construct(RouteCollection $collection, PhpFileLoader $loader, /** * @param string|string[]|null $exclude Glob patterns to exclude from the import */ - final public function import($resource, string $type = null, bool $ignoreErrors = false, $exclude = null): ImportConfigurator + final public function import(string|array $resource, ?string $type = null, bool $ignoreErrors = false, string|array|null $exclude = null): ImportConfigurator { $this->loader->setCurrentDir(\dirname($this->path)); @@ -68,10 +68,7 @@ final public function env(): ?string return $this->env; } - /** - * @return static - */ - final public function withPath(string $path): self + final public function withPath(string $path): static { $clone = clone $this; $clone->path = $clone->file = $path; diff --git a/symfony/routing/Loader/Configurator/Traits/AddTrait.php b/symfony/routing/Loader/Configurator/Traits/AddTrait.php index 92b1bd0ea..5698df5da 100644 --- a/symfony/routing/Loader/Configurator/Traits/AddTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/AddTrait.php @@ -35,7 +35,7 @@ trait AddTrait * * @param string|array $path the path, or the localized paths of the route */ - public function add(string $name, $path): RouteConfigurator + public function add(string $name, string|array $path): RouteConfigurator { $parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null); $route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes); @@ -53,7 +53,7 @@ public function alias(string $name, string $alias): AliasConfigurator * * @param string|array $path the path, or the localized paths of the route */ - public function __invoke(string $name, $path): RouteConfigurator + public function __invoke(string $name, string|array $path): RouteConfigurator { return $this->add($name, $path); } diff --git a/symfony/routing/Loader/Configurator/Traits/HostTrait.php b/symfony/routing/Loader/Configurator/Traits/HostTrait.php index 54ae6566a..d275f6c67 100644 --- a/symfony/routing/Loader/Configurator/Traits/HostTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/HostTrait.php @@ -18,7 +18,7 @@ */ trait HostTrait { - final protected function addHost(RouteCollection $routes, $hosts) + final protected function addHost(RouteCollection $routes, string|array $hosts): void { if (!$hosts || !\is_array($hosts)) { $routes->setHost($hosts ?: ''); diff --git a/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php b/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php index 4734a4eac..a26a73420 100644 --- a/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php @@ -27,7 +27,7 @@ trait LocalizedRouteTrait * * @param string|array $path the path, or the localized paths of the route */ - final protected function createLocalizedRoute(RouteCollection $collection, string $name, $path, string $namePrefix = '', array $prefixes = null): RouteCollection + final protected function createLocalizedRoute(RouteCollection $collection, string $name, string|array $path, string $namePrefix = '', ?array $prefixes = null): RouteCollection { $paths = []; diff --git a/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php b/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php index 27053bcaf..89a65d8f7 100644 --- a/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php @@ -21,7 +21,7 @@ */ trait PrefixTrait { - final protected function addPrefix(RouteCollection $routes, $prefix, bool $trailingSlashOnRoot) + final protected function addPrefix(RouteCollection $routes, string|array $prefix, bool $trailingSlashOnRoot): void { if (\is_array($prefix)) { foreach ($prefix as $locale => $localePrefix) { @@ -29,6 +29,7 @@ final protected function addPrefix(RouteCollection $routes, $prefix, bool $trail } foreach ($routes->all() as $name => $route) { if (null === $locale = $route->getDefault('_locale')) { + $priority = $routes->getPriority($name) ?? 0; $routes->remove($name); foreach ($prefix as $locale => $localePrefix) { $localizedRoute = clone $route; @@ -36,13 +37,13 @@ final protected function addPrefix(RouteCollection $routes, $prefix, bool $trail $localizedRoute->setRequirement('_locale', preg_quote($locale)); $localizedRoute->setDefault('_canonical_route', $name); $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $routes->add($name.'.'.$locale, $localizedRoute); + $routes->add($name.'.'.$locale, $localizedRoute, $priority); } } elseif (!isset($prefix[$locale])) { throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); } else { $route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $routes->add($name, $route); + $routes->add($name, $route, $routes->getPriority($name) ?? 0); } } diff --git a/symfony/routing/Loader/Configurator/Traits/RouteTrait.php b/symfony/routing/Loader/Configurator/Traits/RouteTrait.php index ac05d10e5..16dc43d0a 100644 --- a/symfony/routing/Loader/Configurator/Traits/RouteTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/RouteTrait.php @@ -26,7 +26,7 @@ trait RouteTrait * * @return $this */ - final public function defaults(array $defaults): self + final public function defaults(array $defaults): static { $this->route->addDefaults($defaults); @@ -38,7 +38,7 @@ final public function defaults(array $defaults): self * * @return $this */ - final public function requirements(array $requirements): self + final public function requirements(array $requirements): static { $this->route->addRequirements($requirements); @@ -50,7 +50,7 @@ final public function requirements(array $requirements): self * * @return $this */ - final public function options(array $options): self + final public function options(array $options): static { $this->route->addOptions($options); @@ -62,7 +62,7 @@ final public function options(array $options): self * * @return $this */ - final public function utf8(bool $utf8 = true): self + final public function utf8(bool $utf8 = true): static { $this->route->addOptions(['utf8' => $utf8]); @@ -74,7 +74,7 @@ final public function utf8(bool $utf8 = true): self * * @return $this */ - final public function condition(string $condition): self + final public function condition(string $condition): static { $this->route->setCondition($condition); @@ -86,7 +86,7 @@ final public function condition(string $condition): self * * @return $this */ - final public function host(string $pattern): self + final public function host(string $pattern): static { $this->route->setHost($pattern); @@ -101,7 +101,7 @@ final public function host(string $pattern): self * * @return $this */ - final public function schemes(array $schemes): self + final public function schemes(array $schemes): static { $this->route->setSchemes($schemes); @@ -116,7 +116,7 @@ final public function schemes(array $schemes): self * * @return $this */ - final public function methods(array $methods): self + final public function methods(array $methods): static { $this->route->setMethods($methods); @@ -130,7 +130,7 @@ final public function methods(array $methods): self * * @return $this */ - final public function controller($controller): self + final public function controller(callable|string|array $controller): static { $this->route->addDefaults(['_controller' => $controller]); @@ -142,7 +142,7 @@ final public function controller($controller): self * * @return $this */ - final public function locale(string $locale): self + final public function locale(string $locale): static { $this->route->addDefaults(['_locale' => $locale]); @@ -154,7 +154,7 @@ final public function locale(string $locale): self * * @return $this */ - final public function format(string $format): self + final public function format(string $format): static { $this->route->addDefaults(['_format' => $format]); @@ -166,7 +166,7 @@ final public function format(string $format): self * * @return $this */ - final public function stateless(bool $stateless = true): self + final public function stateless(bool $stateless = true): static { $this->route->addDefaults(['_stateless' => $stateless]); diff --git a/symfony/routing/Loader/ContainerLoader.php b/symfony/routing/Loader/ContainerLoader.php index d8730aec6..af325be08 100644 --- a/symfony/routing/Loader/ContainerLoader.php +++ b/symfony/routing/Loader/ContainerLoader.php @@ -20,26 +20,20 @@ */ class ContainerLoader extends ObjectLoader { - private $container; + private ContainerInterface $container; - public function __construct(ContainerInterface $container, string $env = null) + public function __construct(ContainerInterface $container, ?string $env = null) { $this->container = $container; parent::__construct($env); } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { return 'service' === $type && \is_string($resource); } - /** - * {@inheritdoc} - */ - protected function getObject(string $id) + protected function getObject(string $id): object { return $this->container->get($id); } diff --git a/symfony/routing/Loader/DirectoryLoader.php b/symfony/routing/Loader/DirectoryLoader.php index c0f349177..6c6c48e2e 100644 --- a/symfony/routing/Loader/DirectoryLoader.php +++ b/symfony/routing/Loader/DirectoryLoader.php @@ -17,10 +17,7 @@ class DirectoryLoader extends FileLoader { - /** - * {@inheritdoc} - */ - public function load($file, string $type = null) + public function load(mixed $file, ?string $type = null): mixed { $path = $this->locator->locate($file); @@ -46,12 +43,9 @@ public function load($file, string $type = null) return $collection; } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { - // only when type is forced to directory, not to conflict with AnnotationLoader + // only when type is forced to directory, not to conflict with AttributeLoader return 'directory' === $type; } diff --git a/symfony/routing/Loader/GlobFileLoader.php b/symfony/routing/Loader/GlobFileLoader.php index 780fb15dc..65afa5a37 100644 --- a/symfony/routing/Loader/GlobFileLoader.php +++ b/symfony/routing/Loader/GlobFileLoader.php @@ -21,10 +21,7 @@ */ class GlobFileLoader extends FileLoader { - /** - * {@inheritdoc} - */ - public function load($resource, string $type = null) + public function load(mixed $resource, ?string $type = null): mixed { $collection = new RouteCollection(); @@ -37,10 +34,7 @@ public function load($resource, string $type = null) return $collection; } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { return 'glob' === $type; } diff --git a/symfony/routing/Loader/ObjectLoader.php b/symfony/routing/Loader/ObjectLoader.php index 062453908..c2ad6a03f 100644 --- a/symfony/routing/Loader/ObjectLoader.php +++ b/symfony/routing/Loader/ObjectLoader.php @@ -27,20 +27,13 @@ abstract class ObjectLoader extends Loader * * For example, if your application uses a service container, * the $id may be a service id. - * - * @return object */ - abstract protected function getObject(string $id); + abstract protected function getObject(string $id): object; /** * Calls the object method that will load the routes. - * - * @param string $resource object_id::method - * @param string|null $type The resource type - * - * @return RouteCollection */ - public function load($resource, string $type = null) + public function load(mixed $resource, ?string $type = null): RouteCollection { if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) { throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object')); @@ -73,7 +66,7 @@ public function load($resource, string $type = null) return $routeCollection; } - private function addClassResource(\ReflectionClass $class, RouteCollection $collection) + private function addClassResource(\ReflectionClass $class, RouteCollection $collection): void { do { if (is_file($class->getFileName())) { diff --git a/symfony/routing/Loader/PhpFileLoader.php b/symfony/routing/Loader/PhpFileLoader.php index 39ac81273..adf7eed3f 100644 --- a/symfony/routing/Loader/PhpFileLoader.php +++ b/symfony/routing/Loader/PhpFileLoader.php @@ -29,13 +29,8 @@ class PhpFileLoader extends FileLoader { /** * Loads a PHP file. - * - * @param string $file A PHP file path - * @param string|null $type The resource type - * - * @return RouteCollection */ - public function load($file, string $type = null) + public function load(mixed $file, ?string $type = null): RouteCollection { $path = $this->locator->locate($file); $this->setCurrentDir(\dirname($path)); @@ -59,10 +54,7 @@ public function load($file, string $type = null) return $collection; } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type); } diff --git a/symfony/routing/Loader/Psr4DirectoryLoader.php b/symfony/routing/Loader/Psr4DirectoryLoader.php new file mode 100644 index 000000000..bbf99418c --- /dev/null +++ b/symfony/routing/Loader/Psr4DirectoryLoader.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Loader\DirectoryAwareLoaderInterface; +use Symfony\Component\Config\Loader\Loader; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * A loader that discovers controller classes in a directory that follows PSR-4. + * + * @author Alexander M. Turek + */ +final class Psr4DirectoryLoader extends Loader implements DirectoryAwareLoaderInterface +{ + private ?string $currentDirectory = null; + + public function __construct( + private readonly FileLocatorInterface $locator, + ) { + // PSR-4 directory loader has no env-aware logic, so we drop the $env constructor parameter. + parent::__construct(); + } + + /** + * @param array{path: string, namespace: string} $resource + */ + public function load(mixed $resource, ?string $type = null): ?RouteCollection + { + $path = $this->locator->locate($resource['path'], $this->currentDirectory); + if (!is_dir($path)) { + return new RouteCollection(); + } + + return $this->loadFromDirectory($path, trim($resource['namespace'], '\\')); + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return ('attribute' === $type || 'annotation' === $type) && \is_array($resource) && isset($resource['path'], $resource['namespace']); + } + + public function forDirectory(string $currentDirectory): static + { + $loader = clone $this; + $loader->currentDirectory = $currentDirectory; + + return $loader; + } + + private function loadFromDirectory(string $directory, string $psr4Prefix): RouteCollection + { + $collection = new RouteCollection(); + $collection->addResource(new DirectoryResource($directory, '/\.php$/')); + $files = iterator_to_array(new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + fn (\SplFileInfo $current) => !str_starts_with($current->getBasename(), '.') + ), + \RecursiveIteratorIterator::SELF_FIRST + )); + usort($files, fn (\SplFileInfo $a, \SplFileInfo $b) => (string) $a > (string) $b ? 1 : -1); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + if ($file->isDir()) { + $collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename())); + + continue; + } + if ('php' !== $file->getExtension() || !class_exists($className = $psr4Prefix.'\\'.$file->getBasename('.php')) || (new \ReflectionClass($className))->isAbstract()) { + continue; + } + + $collection->addCollection($this->import($className, 'attribute')); + } + + return $collection; + } +} diff --git a/symfony/routing/Loader/XmlFileLoader.php b/symfony/routing/Loader/XmlFileLoader.php index 220153364..2518161ae 100644 --- a/symfony/routing/Loader/XmlFileLoader.php +++ b/symfony/routing/Loader/XmlFileLoader.php @@ -35,17 +35,10 @@ class XmlFileLoader extends FileLoader public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd'; /** - * Loads an XML file. - * - * @param string $file An XML file path - * @param string|null $type The resource type - * - * @return RouteCollection - * * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be * parsed because it does not validate against the scheme */ - public function load($file, string $type = null) + public function load(mixed $file, ?string $type = null): RouteCollection { $path = $this->locator->locate($file); @@ -69,6 +62,8 @@ public function load($file, string $type = null) /** * Parses a node from a loaded XML file. * + * @return void + * * @throws \InvalidArgumentException When the XML is invalid */ protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file) @@ -99,10 +94,7 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, str } } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type); } @@ -110,6 +102,8 @@ public function supports($resource, string $type = null) /** * Parses a route and adds it to the RouteCollection. * + * @return void + * * @throws \InvalidArgumentException When the XML is invalid */ protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path) @@ -157,12 +151,23 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $node, st /** * Parses an import and adds the routes in the resource to the RouteCollection. * + * @return void + * * @throws \InvalidArgumentException When the XML is invalid */ protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file) { - if ('' === $resource = $node->getAttribute('resource')) { - throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "resource" attribute.', $path)); + /** @var \DOMElement $resourceElement */ + if (!($resource = $node->getAttribute('resource') ?: null) && $resourceElement = $node->getElementsByTagName('resource')[0] ?? null) { + $resource = []; + /** @var \DOMAttr $attribute */ + foreach ($resourceElement->attributes as $attribute) { + $resource[$attribute->name] = $attribute->value; + } + } + + if (!$resource) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "resource" attribute or element.', $path)); } $type = $node->getAttribute('type'); @@ -229,13 +234,11 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, s } /** - * @return \DOMDocument - * * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors * or when the XML structure is not as expected by the scheme - * see validate() */ - protected function loadFile(string $file) + protected function loadFile(string $file): \DOMDocument { return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH); } @@ -288,6 +291,8 @@ private function parseConfigs(\DOMElement $node, string $path): array case 'condition': $condition = trim($n->textContent); break; + case 'resource': + break; default: throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path)); } @@ -330,10 +335,8 @@ private function parseConfigs(\DOMElement $node, string $path): array /** * Parses the "default" elements. - * - * @return array|bool|float|int|string|null */ - private function parseDefaultsConfig(\DOMElement $element, string $path) + private function parseDefaultsConfig(\DOMElement $element, string $path): array|bool|float|int|string|null { if ($this->isElementValueNull($element)) { return null; @@ -363,11 +366,9 @@ private function parseDefaultsConfig(\DOMElement $element, string $path) /** * Recursively parses the value of a "default" element. * - * @return array|bool|float|int|string|null - * * @throws \InvalidArgumentException when the XML is invalid */ - private function parseDefaultNode(\DOMElement $node, string $path) + private function parseDefaultNode(\DOMElement $node, string $path): array|bool|float|int|string|null { if ($this->isElementValueNull($node)) { return null; diff --git a/symfony/routing/Loader/YamlFileLoader.php b/symfony/routing/Loader/YamlFileLoader.php index ae98a314e..9605e9a87 100644 --- a/symfony/routing/Loader/YamlFileLoader.php +++ b/symfony/routing/Loader/YamlFileLoader.php @@ -36,19 +36,12 @@ class YamlFileLoader extends FileLoader private const AVAILABLE_KEYS = [ 'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude', 'stateless', ]; - private $yamlParser; + private YamlParser $yamlParser; /** - * Loads a Yaml file. - * - * @param string $file A Yaml file path - * @param string|null $type The resource type - * - * @return RouteCollection - * * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid */ - public function load($file, string $type = null) + public function load(mixed $file, ?string $type = null): RouteCollection { $path = $this->locator->locate($file); @@ -60,9 +53,7 @@ public function load($file, string $type = null) throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path)); } - if (null === $this->yamlParser) { - $this->yamlParser = new YamlParser(); - } + $this->yamlParser ??= new YamlParser(); try { $parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT); @@ -84,7 +75,7 @@ public function load($file, string $type = null) } foreach ($parsedConfig as $name => $config) { - if (0 === strpos($name, 'when@')) { + if (str_starts_with($name, 'when@')) { if (!$this->env || 'when@'.$this->env !== $name) { continue; } @@ -114,16 +105,15 @@ public function load($file, string $type = null) return $collection; } - /** - * {@inheritdoc} - */ - public function supports($resource, string $type = null) + public function supports(mixed $resource, ?string $type = null): bool { return \is_string($resource) && \in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['yml', 'yaml'], true) && (!$type || 'yaml' === $type); } /** * Parses a route and adds it to the RouteCollection. + * + * @return void */ protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path) { @@ -182,6 +172,8 @@ protected function parseRoute(RouteCollection $collection, string $name, array $ /** * Parses an import and adds the routes in the resource to the RouteCollection. + * + * @return void */ protected function parseImport(RouteCollection $collection, array $config, string $path, string $file) { @@ -250,16 +242,12 @@ protected function parseImport(RouteCollection $collection, array $config, strin } /** - * Validates the route configuration. - * - * @param array $config A resource config - * @param string $name The config key - * @param string $path The loaded file path + * @return void * * @throws \InvalidArgumentException If one of the provided config keys is not supported, * something is missing or the combination is nonsense */ - protected function validate($config, string $name, string $path) + protected function validate(mixed $config, string $name, string $path) { if (!\is_array($config)) { throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); diff --git a/symfony/routing/Loader/schema/routing/routing-1.0.xsd b/symfony/routing/Loader/schema/routing/routing-1.0.xsd index 66c40a0d8..1b24dfdc8 100644 --- a/symfony/routing/Loader/schema/routing/routing-1.0.xsd +++ b/symfony/routing/Loader/schema/routing/routing-1.0.xsd @@ -76,8 +76,9 @@ + - + @@ -93,6 +94,12 @@ + + + + + + diff --git a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php index 4b3888045..a7639cd4c 100644 --- a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -26,18 +26,15 @@ */ class CompiledUrlMatcherDumper extends MatcherDumper { - private $expressionLanguage; - private $signalingException; + private ExpressionLanguage $expressionLanguage; + private ?\Exception $signalingException = null; /** * @var ExpressionFunctionProviderInterface[] */ - private $expressionLanguageProviders = []; + private array $expressionLanguageProviders = []; - /** - * {@inheritdoc} - */ - public function dump(array $options = []) + public function dump(array $options = []): string { return <<expressionLanguageProviders[] = $provider; @@ -115,7 +115,7 @@ public function getCompiledRoutes(bool $forDump = false): array } $checkConditionCode = <<indent(implode("\n", $conditions), 3)} } @@ -139,8 +139,7 @@ private function generateCompiledRoutes(): string foreach ($staticRoutes as $path => $routes) { $code .= sprintf(" %s => [\n", self::export($path)); foreach ($routes as $route) { - $r = array_map([__CLASS__, 'export'], $route); - $code .= sprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", $r[0], $r[1], $r[2], $r[3], $r[4], $r[5], $r[6]); + $code .= vsprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", array_map([__CLASS__, 'export'], $route)); } $code .= " ],\n"; } @@ -152,8 +151,7 @@ private function generateCompiledRoutes(): string foreach ($dynamicRoutes as $path => $routes) { $code .= sprintf(" %s => [\n", self::export($path)); foreach ($routes as $route) { - $r = array_map([__CLASS__, 'export'], $route); - $code .= sprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", $r[0], $r[1], $r[2], $r[3], $r[4], $r[5], $r[6]); + $code .= vsprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", array_map([__CLASS__, 'export'], $route)); } $code .= " ],\n"; } @@ -334,7 +332,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo if ($hasTrailingSlash = '/' !== $regex && '/' === $regex[-1]) { $regex = substr($regex, 0, -1); } - $hasTrailingVar = (bool) preg_match('#\{\w+\}/?$#', $route->getPath()); + $hasTrailingVar = (bool) preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); $tree->addRoute($regex, [$name, $regex, $state->vars, $route, $hasTrailingSlash, $hasTrailingVar]); } @@ -351,7 +349,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo $state->markTail = 0; // if the regex is too large, throw a signaling exception to recompute with smaller chunk size - set_error_handler(function ($type, $message) { throw str_contains($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message); }); + set_error_handler(fn ($type, $message) => throw str_contains($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message)); try { preg_match($state->regex, ''); } finally { @@ -418,7 +416,7 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st /** * Compiles a single Route to PHP code used to match it against the path info. */ - private function compileRoute(Route $route, string $name, $vars, bool $hasTrailingSlash, bool $hasTrailingVar, array &$conditions): array + private function compileRoute(Route $route, string $name, string|array|null $vars, bool $hasTrailingSlash, bool $hasTrailingVar, array &$conditions): array { $defaults = $route->getDefaults(); @@ -428,8 +426,8 @@ private function compileRoute(Route $route, string $name, $vars, bool $hasTraili } if ($condition = $route->getCondition()) { - $condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request']); - $condition = $conditions[$condition] ?? $conditions[$condition] = (str_contains($condition, '$request') ? 1 : -1) * \count($conditions); + $condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request', 'params']); + $condition = $conditions[$condition] ??= (str_contains($condition, '$request') ? 1 : -1) * \count($conditions); } else { $condition = null; } @@ -447,9 +445,9 @@ private function compileRoute(Route $route, string $name, $vars, bool $hasTraili private function getExpressionLanguage(): ExpressionLanguage { - if (null === $this->expressionLanguage) { + if (!isset($this->expressionLanguage)) { if (!class_exists(ExpressionLanguage::class)) { - throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); } $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); } @@ -465,7 +463,7 @@ private function indent(string $code, int $level = 1): string /** * @internal */ - public static function export($value): string + public static function export(mixed $value): string { if (null === $value) { return 'null'; diff --git a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php index bdb7ba3d0..50abf4587 100644 --- a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +++ b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php @@ -26,15 +26,11 @@ */ trait CompiledUrlMatcherTrait { - private $matchHost = false; - private $staticRoutes = []; - private $regexpList = []; - private $dynamicRoutes = []; - - /** - * @var callable|null - */ - private $checkCondition; + private bool $matchHost = false; + private array $staticRoutes = []; + private array $regexpList = []; + private array $dynamicRoutes = []; + private ?\Closure $checkCondition; public function match(string $pathinfo): array { @@ -92,10 +88,6 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche $supportsRedirections = 'GET' === $canonicalMethod && $this instanceof RedirectableUrlMatcherInterface; foreach ($this->staticRoutes[$trimmedPathinfo] ?? [] as [$ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash, , $condition]) { - if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ?? $request = $this->request ?: $this->createRequest($pathinfo) : null)) { - continue; - } - if ($requiredHost) { if ('{' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) { continue; @@ -106,6 +98,10 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche } } + if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ??= $this->request ?: $this->createRequest($pathinfo) : null, $ret)) { + continue; + } + if ('/' !== $pathinfo && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) { return $allow = $allowSchemes = []; @@ -132,13 +128,8 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche foreach ($this->regexpList as $offset => $regex) { while (preg_match($regex, $matchedPathinfo, $matches)) { foreach ($this->dynamicRoutes[$m = (int) $matches['MARK']] as [$ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash, $hasTrailingVar, $condition]) { - if (null !== $condition) { - if (0 === $condition) { // marks the last route in the regexp - continue 3; - } - if (!($this->checkCondition)($condition, $context, 0 < $condition ? $request ?? $request = $this->request ?: $this->createRequest($pathinfo) : null)) { - continue; - } + if (0 === $condition) { // marks the last route in the regexp + continue 3; } $hasTrailingVar = $trimmedPathinfo !== $pathinfo && $hasTrailingVar; @@ -151,17 +142,21 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche } } - if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { - if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) { - return $allow = $allowSchemes = []; + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; } + } + + if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ??= $this->request ?: $this->createRequest($pathinfo) : null, $ret)) { continue; } - foreach ($vars as $i => $v) { - if (isset($matches[1 + $i])) { - $ret[$v] = $matches[1 + $i]; + if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { + if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) { + return $allow = $allowSchemes = []; } + continue; } if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { diff --git a/symfony/routing/Matcher/Dumper/MatcherDumper.php b/symfony/routing/Matcher/Dumper/MatcherDumper.php index ea51ab406..085f3ba3a 100644 --- a/symfony/routing/Matcher/Dumper/MatcherDumper.php +++ b/symfony/routing/Matcher/Dumper/MatcherDumper.php @@ -20,17 +20,14 @@ */ abstract class MatcherDumper implements MatcherDumperInterface { - private $routes; + private RouteCollection $routes; public function __construct(RouteCollection $routes) { $this->routes = $routes; } - /** - * {@inheritdoc} - */ - public function getRoutes() + public function getRoutes(): RouteCollection { return $this->routes; } diff --git a/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php b/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php index 8e33802d3..92cc4db2a 100644 --- a/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php +++ b/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php @@ -23,15 +23,11 @@ interface MatcherDumperInterface /** * Dumps a set of routes to a string representation of executable code * that can then be used to match a request against these routes. - * - * @return string */ - public function dump(array $options = []); + public function dump(array $options = []): string; /** * Gets the routes to dump. - * - * @return RouteCollection */ - public function getRoutes(); + public function getRoutes(): RouteCollection; } diff --git a/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php b/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php index 97bd692a5..42ca799f6 100644 --- a/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php +++ b/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php @@ -23,22 +23,22 @@ */ class StaticPrefixCollection { - private $prefix; + private string $prefix; /** * @var string[] */ - private $staticPrefixes = []; + private array $staticPrefixes = []; /** * @var string[] */ - private $prefixes = []; + private array $prefixes = []; /** * @var array[]|self[] */ - private $items = []; + private array $items = []; public function __construct(string $prefix = '/') { @@ -60,10 +60,8 @@ public function getRoutes(): array /** * Adds a route to a group. - * - * @param array|self $route */ - public function addRoute(string $prefix, $route) + public function addRoute(string $prefix, array|self $route): void { [$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix); @@ -149,12 +147,12 @@ private function getCommonPrefix(string $prefix, string $anotherPrefix): array $baseLength = \strlen($this->prefix); $end = min(\strlen($prefix), \strlen($anotherPrefix)); $staticLength = null; - set_error_handler([__CLASS__, 'handleError']); + set_error_handler(self::handleError(...)); try { for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) { if ('(' === $prefix[$i]) { - $staticLength = $staticLength ?? $i; + $staticLength ??= $i; for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) { if ($prefix[$j] !== $anotherPrefix[$j]) { break 2; @@ -198,8 +196,9 @@ private function getCommonPrefix(string $prefix, string $anotherPrefix): array return [substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i)]; } - public static function handleError(int $type, string $msg) + public static function handleError(int $type, string $msg): bool { - return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length'); + return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length') + || str_contains($msg, 'Compilation failed: length of lookbehind assertion is not limited'); } } diff --git a/symfony/routing/Matcher/ExpressionLanguageProvider.php b/symfony/routing/Matcher/ExpressionLanguageProvider.php index 96bb7babf..3aeebe699 100644 --- a/symfony/routing/Matcher/ExpressionLanguageProvider.php +++ b/symfony/routing/Matcher/ExpressionLanguageProvider.php @@ -22,29 +22,22 @@ */ class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface { - private $functions; + private ServiceProviderInterface $functions; public function __construct(ServiceProviderInterface $functions) { $this->functions = $functions; } - /** - * {@inheritdoc} - */ - public function getFunctions() + public function getFunctions(): array { $functions = []; foreach ($this->functions->getProvidedServices() as $function => $type) { $functions[] = new ExpressionFunction( $function, - static function (...$args) use ($function) { - return sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args)); - }, - function ($values, ...$args) use ($function) { - return $values['context']->getParameter('_functions')->get($function)(...$args); - } + static fn (...$args) => sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args)), + fn ($values, ...$args) => $values['context']->getParameter('_functions')->get($function)(...$args) ); } diff --git a/symfony/routing/Matcher/RedirectableUrlMatcher.php b/symfony/routing/Matcher/RedirectableUrlMatcher.php index 3cd7c81a6..8d1ad4f90 100644 --- a/symfony/routing/Matcher/RedirectableUrlMatcher.php +++ b/symfony/routing/Matcher/RedirectableUrlMatcher.php @@ -19,10 +19,7 @@ */ abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface { - /** - * {@inheritdoc} - */ - public function match(string $pathinfo) + public function match(string $pathinfo): array { try { return parent::match($pathinfo); @@ -39,7 +36,7 @@ public function match(string $pathinfo) $ret = parent::match($pathinfo); return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret; - } catch (ExceptionInterface $e2) { + } catch (ExceptionInterface) { throw $e; } finally { $this->context->setScheme($scheme); @@ -52,7 +49,7 @@ public function match(string $pathinfo) $ret = parent::match($pathinfo); return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret; - } catch (ExceptionInterface $e2) { + } catch (ExceptionInterface) { if ($this->allowSchemes) { goto redirect_scheme; } diff --git a/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php b/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php index d07f42093..e4bcedda0 100644 --- a/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php +++ b/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php @@ -24,8 +24,6 @@ interface RedirectableUrlMatcherInterface * @param string $path The path info to redirect to * @param string $route The route name that matched * @param string|null $scheme The URL scheme (null to keep the current one) - * - * @return array */ - public function redirect(string $path, string $route, string $scheme = null); + public function redirect(string $path, string $route, ?string $scheme = null): array; } diff --git a/symfony/routing/Matcher/RequestMatcherInterface.php b/symfony/routing/Matcher/RequestMatcherInterface.php index c05016e82..febba95bd 100644 --- a/symfony/routing/Matcher/RequestMatcherInterface.php +++ b/symfony/routing/Matcher/RequestMatcherInterface.php @@ -29,11 +29,9 @@ interface RequestMatcherInterface * If the matcher cannot find information, it must throw one of the exceptions documented * below. * - * @return array - * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If no matching resource could be found * @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed */ - public function matchRequest(Request $request); + public function matchRequest(Request $request): array; } diff --git a/symfony/routing/Matcher/TraceableUrlMatcher.php b/symfony/routing/Matcher/TraceableUrlMatcher.php index 9e8c4c42d..3c7e24d06 100644 --- a/symfony/routing/Matcher/TraceableUrlMatcher.php +++ b/symfony/routing/Matcher/TraceableUrlMatcher.php @@ -29,18 +29,24 @@ class TraceableUrlMatcher extends UrlMatcher protected $traces; + /** + * @return array + */ public function getTraces(string $pathinfo) { $this->traces = []; try { $this->match($pathinfo); - } catch (ExceptionInterface $e) { + } catch (ExceptionInterface) { } return $this->traces; } + /** + * @return array + */ public function getTracesForRequest(Request $request) { $this->request = $request; @@ -50,7 +56,7 @@ public function getTracesForRequest(Request $request) return $traces; } - protected function matchCollection(string $pathinfo, RouteCollection $routes) + protected function matchCollection(string $pathinfo, RouteCollection $routes): array { // HEAD and GET are equivalent as per RFC if ('HEAD' === $method = $this->context->getMethod()) { @@ -99,7 +105,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) continue; } - $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath()); + $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) { if ($hasTrailingSlash) { @@ -115,7 +121,9 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) continue; } - $status = $this->handleRouteRequirements($pathinfo, $name, $route); + $attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); + + $status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes); if (self::REQUIREMENT_MISMATCH === $status[0]) { $this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $route->getCondition()), self::ROUTE_ALMOST_MATCHES, $name, $route); @@ -146,19 +154,19 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); - return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? [])); + return array_replace($attributes, $status[1] ?? []); } return []; } - private function addTrace(string $log, int $level = self::ROUTE_DOES_NOT_MATCH, string $name = null, Route $route = null) + private function addTrace(string $log, int $level = self::ROUTE_DOES_NOT_MATCH, ?string $name = null, ?Route $route = null): void { $this->traces[] = [ 'log' => $log, 'name' => $name, 'level' => $level, - 'path' => null !== $route ? $route->getPath() : null, + 'path' => $route?->getPath(), ]; } } diff --git a/symfony/routing/Matcher/UrlMatcher.php b/symfony/routing/Matcher/UrlMatcher.php index f076a2f5e..778d154ed 100644 --- a/symfony/routing/Matcher/UrlMatcher.php +++ b/symfony/routing/Matcher/UrlMatcher.php @@ -45,7 +45,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface * * @internal */ - protected $allowSchemes = []; + protected array $allowSchemes = []; protected $routes; protected $request; @@ -63,25 +63,19 @@ public function __construct(RouteCollection $routes, RequestContext $context) } /** - * {@inheritdoc} + * @return void */ public function setContext(RequestContext $context) { $this->context = $context; } - /** - * {@inheritdoc} - */ - public function getContext() + public function getContext(): RequestContext { return $this->context; } - /** - * {@inheritdoc} - */ - public function match(string $pathinfo) + public function match(string $pathinfo): array { $this->allow = $this->allowSchemes = []; @@ -96,10 +90,7 @@ public function match(string $pathinfo) throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); } - /** - * {@inheritdoc} - */ - public function matchRequest(Request $request) + public function matchRequest(Request $request): array { $this->request = $request; @@ -110,6 +101,9 @@ public function matchRequest(Request $request) return $ret; } + /** + * @return void + */ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; @@ -120,13 +114,11 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac * * @param string $pathinfo The path info to be parsed * - * @return array - * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If the resource could not be found * @throws MethodNotAllowedException If the resource was found but the request method is not allowed */ - protected function matchCollection(string $pathinfo, RouteCollection $routes) + protected function matchCollection(string $pathinfo, RouteCollection $routes): array { // HEAD and GET are equivalent as per RFC if ('HEAD' === $method = $this->context->getMethod()) { @@ -154,7 +146,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) continue; } - $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath()); + $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) { if ($hasTrailingSlash) { @@ -169,7 +161,9 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) continue; } - $status = $this->handleRouteRequirements($pathinfo, $name, $route); + $attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); + + $status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes); if (self::REQUIREMENT_MISMATCH === $status[0]) { continue; @@ -192,7 +186,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) continue; } - return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? [])); + return array_replace($attributes, $status[1] ?? []); } return []; @@ -204,10 +198,8 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes) * As this method requires the Route object, it is not available * in matchers that do not have access to the matched Route instance * (like the PHP and Apache matcher dumpers). - * - * @return array */ - protected function getAttributes(Route $route, string $name, array $attributes) + protected function getAttributes(Route $route, string $name, array $attributes): array { $defaults = $route->getDefaults(); if (isset($defaults['_canonical_route'])) { @@ -224,10 +216,25 @@ protected function getAttributes(Route $route, string $name, array $attributes) * * @return array The first element represents the status, the second contains additional information */ - protected function handleRouteRequirements(string $pathinfo, string $name, Route $route) + protected function handleRouteRequirements(string $pathinfo, string $name, Route $route/* , array $routeParameters */): array { + if (\func_num_args() < 4) { + trigger_deprecation('symfony/routing', '6.1', 'The "%s()" method will have a new "array $routeParameters" argument in version 7.0, not defining it is deprecated.', __METHOD__); + $routeParameters = []; + } else { + $routeParameters = func_get_arg(3); + + if (!\is_array($routeParameters)) { + throw new \TypeError(sprintf('"%s": Argument $routeParameters is expected to be an array, got "%s".', __METHOD__, get_debug_type($routeParameters))); + } + } + // expression condition - if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { + if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), [ + 'context' => $this->context, + 'request' => $this->request ?: $this->createRequest($pathinfo), + 'params' => $routeParameters, + ])) { return [self::REQUIREMENT_MISMATCH, null]; } @@ -236,10 +243,8 @@ protected function handleRouteRequirements(string $pathinfo, string $name, Route /** * Get merged default parameters. - * - * @return array */ - protected function mergeDefaults(array $params, array $defaults) + protected function mergeDefaults(array $params, array $defaults): array { foreach ($params as $key => $value) { if (!\is_int($key) && null !== $value) { @@ -250,11 +255,14 @@ protected function mergeDefaults(array $params, array $defaults) return $defaults; } + /** + * @return ExpressionLanguage + */ protected function getExpressionLanguage() { - if (null === $this->expressionLanguage) { + if (!isset($this->expressionLanguage)) { if (!class_exists(ExpressionLanguage::class)) { - throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); } $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); } diff --git a/symfony/routing/Matcher/UrlMatcherInterface.php b/symfony/routing/Matcher/UrlMatcherInterface.php index 0a5be9744..68a3737fb 100644 --- a/symfony/routing/Matcher/UrlMatcherInterface.php +++ b/symfony/routing/Matcher/UrlMatcherInterface.php @@ -31,11 +31,9 @@ interface UrlMatcherInterface extends RequestContextAwareInterface * * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded) * - * @return array - * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If the resource could not be found * @throws MethodNotAllowedException If the resource was found but the request method is not allowed */ - public function match(string $pathinfo); + public function match(string $pathinfo): array; } diff --git a/symfony/routing/RequestContext.php b/symfony/routing/RequestContext.php index f54c430ee..e3f4831b3 100644 --- a/symfony/routing/RequestContext.php +++ b/symfony/routing/RequestContext.php @@ -23,15 +23,15 @@ */ class RequestContext { - private $baseUrl; - private $pathInfo; - private $method; - private $host; - private $scheme; - private $httpPort; - private $httpsPort; - private $queryString; - private $parameters = []; + private string $baseUrl; + private string $pathInfo; + private string $method; + private string $host; + private string $scheme; + private int $httpPort; + private int $httpsPort; + private string $queryString; + private array $parameters = []; public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '') { @@ -67,7 +67,7 @@ public static function fromUri(string $uri, string $host = 'localhost', string $ * * @return $this */ - public function fromRequest(Request $request) + public function fromRequest(Request $request): static { $this->setBaseUrl($request->getBaseUrl()); $this->setPathInfo($request->getPathInfo()); @@ -83,10 +83,8 @@ public function fromRequest(Request $request) /** * Gets the base URL. - * - * @return string */ - public function getBaseUrl() + public function getBaseUrl(): string { return $this->baseUrl; } @@ -96,7 +94,7 @@ public function getBaseUrl() * * @return $this */ - public function setBaseUrl(string $baseUrl) + public function setBaseUrl(string $baseUrl): static { $this->baseUrl = rtrim($baseUrl, '/'); @@ -105,10 +103,8 @@ public function setBaseUrl(string $baseUrl) /** * Gets the path info. - * - * @return string */ - public function getPathInfo() + public function getPathInfo(): string { return $this->pathInfo; } @@ -118,7 +114,7 @@ public function getPathInfo() * * @return $this */ - public function setPathInfo(string $pathInfo) + public function setPathInfo(string $pathInfo): static { $this->pathInfo = $pathInfo; @@ -129,10 +125,8 @@ public function setPathInfo(string $pathInfo) * Gets the HTTP method. * * The method is always an uppercased string. - * - * @return string */ - public function getMethod() + public function getMethod(): string { return $this->method; } @@ -142,7 +136,7 @@ public function getMethod() * * @return $this */ - public function setMethod(string $method) + public function setMethod(string $method): static { $this->method = strtoupper($method); @@ -153,10 +147,8 @@ public function setMethod(string $method) * Gets the HTTP host. * * The host is always lowercased because it must be treated case-insensitive. - * - * @return string */ - public function getHost() + public function getHost(): string { return $this->host; } @@ -166,7 +158,7 @@ public function getHost() * * @return $this */ - public function setHost(string $host) + public function setHost(string $host): static { $this->host = strtolower($host); @@ -175,10 +167,8 @@ public function setHost(string $host) /** * Gets the HTTP scheme. - * - * @return string */ - public function getScheme() + public function getScheme(): string { return $this->scheme; } @@ -188,7 +178,7 @@ public function getScheme() * * @return $this */ - public function setScheme(string $scheme) + public function setScheme(string $scheme): static { $this->scheme = strtolower($scheme); @@ -197,10 +187,8 @@ public function setScheme(string $scheme) /** * Gets the HTTP port. - * - * @return int */ - public function getHttpPort() + public function getHttpPort(): int { return $this->httpPort; } @@ -210,7 +198,7 @@ public function getHttpPort() * * @return $this */ - public function setHttpPort(int $httpPort) + public function setHttpPort(int $httpPort): static { $this->httpPort = $httpPort; @@ -219,10 +207,8 @@ public function setHttpPort(int $httpPort) /** * Gets the HTTPS port. - * - * @return int */ - public function getHttpsPort() + public function getHttpsPort(): int { return $this->httpsPort; } @@ -232,7 +218,7 @@ public function getHttpsPort() * * @return $this */ - public function setHttpsPort(int $httpsPort) + public function setHttpsPort(int $httpsPort): static { $this->httpsPort = $httpsPort; @@ -241,10 +227,8 @@ public function setHttpsPort(int $httpsPort) /** * Gets the query string without the "?". - * - * @return string */ - public function getQueryString() + public function getQueryString(): string { return $this->queryString; } @@ -254,7 +238,7 @@ public function getQueryString() * * @return $this */ - public function setQueryString(?string $queryString) + public function setQueryString(?string $queryString): static { // string cast to be fault-tolerant, accepting null $this->queryString = (string) $queryString; @@ -264,10 +248,8 @@ public function setQueryString(?string $queryString) /** * Returns the parameters. - * - * @return array */ - public function getParameters() + public function getParameters(): array { return $this->parameters; } @@ -279,7 +261,7 @@ public function getParameters() * * @return $this */ - public function setParameters(array $parameters) + public function setParameters(array $parameters): static { $this->parameters = $parameters; @@ -288,20 +270,16 @@ public function setParameters(array $parameters) /** * Gets a parameter value. - * - * @return mixed */ - public function getParameter(string $name) + public function getParameter(string $name): mixed { return $this->parameters[$name] ?? null; } /** * Checks if a parameter value is set for the given parameter. - * - * @return bool */ - public function hasParameter(string $name) + public function hasParameter(string $name): bool { return \array_key_exists($name, $this->parameters); } @@ -309,11 +287,9 @@ public function hasParameter(string $name) /** * Sets a parameter value. * - * @param mixed $parameter The parameter value - * * @return $this */ - public function setParameter(string $name, $parameter) + public function setParameter(string $name, mixed $parameter): static { $this->parameters[$name] = $parameter; diff --git a/symfony/routing/RequestContextAwareInterface.php b/symfony/routing/RequestContextAwareInterface.php index 270a2b084..04acbdc80 100644 --- a/symfony/routing/RequestContextAwareInterface.php +++ b/symfony/routing/RequestContextAwareInterface.php @@ -15,13 +15,13 @@ interface RequestContextAwareInterface { /** * Sets the request context. + * + * @return void */ public function setContext(RequestContext $context); /** * Gets the request context. - * - * @return RequestContext */ - public function getContext(); + public function getContext(): RequestContext; } diff --git a/symfony/routing/Requirement/EnumRequirement.php b/symfony/routing/Requirement/EnumRequirement.php new file mode 100644 index 000000000..3ab2ed332 --- /dev/null +++ b/symfony/routing/Requirement/EnumRequirement.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Requirement; + +use Symfony\Component\Routing\Exception\InvalidArgumentException; + +final class EnumRequirement implements \Stringable +{ + private string $requirement; + + /** + * @template T of \BackedEnum + * + * @param class-string|list $cases + */ + public function __construct(string|array $cases = []) + { + if (\is_string($cases)) { + if (!is_subclass_of($cases, \BackedEnum::class, true)) { + throw new InvalidArgumentException(sprintf('"%s" is not a "BackedEnum" class.', $cases)); + } + + $cases = $cases::cases(); + } else { + $class = null; + + foreach ($cases as $case) { + if (!$case instanceof \BackedEnum) { + throw new InvalidArgumentException(sprintf('Case must be a "BackedEnum" instance, "%s" given.', get_debug_type($case))); + } + + $class ??= $case::class; + + if (!$case instanceof $class) { + throw new InvalidArgumentException(sprintf('"%s::%s" is not a case of "%s".', get_debug_type($case), $case->name, $class)); + } + } + } + + $this->requirement = implode('|', array_map(static fn ($e) => preg_quote($e->value), $cases)); + } + + public function __toString(): string + { + return $this->requirement; + } +} diff --git a/symfony/routing/Requirement/Requirement.php b/symfony/routing/Requirement/Requirement.php new file mode 100644 index 000000000..dfbb801f2 --- /dev/null +++ b/symfony/routing/Requirement/Requirement.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Requirement; + +/* + * A collection of universal regular-expression constants to use as route parameter requirements. + */ +enum Requirement +{ + public const ASCII_SLUG = '[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*'; // symfony/string AsciiSlugger default implementation + public const CATCH_ALL = '.+'; + public const DATE_YMD = '[0-9]{4}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|(? $requirements An array of requirements for parameters (regexes) + * @param array $options An array of options + * @param string|null $host The host pattern to match + * @param string|string[] $schemes A required URI scheme or an array of restricted schemes + * @param string|string[] $methods A required HTTP method or an array of restricted methods + * @param string|null $condition A condition that should evaluate to true for the route to match */ - public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', $schemes = [], $methods = [], ?string $condition = '') + public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', string|array $schemes = [], string|array $methods = [], ?string $condition = '') { $this->setPath($path); $this->addDefaults($defaults); @@ -82,7 +78,7 @@ public function __serialize(): array */ final public function serialize(): string { - return serialize($this->__serialize()); + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __unserialize(array $data): void @@ -106,15 +102,12 @@ public function __unserialize(array $data): void /** * @internal */ - final public function unserialize($serialized) + final public function unserialize(string $serialized): void { $this->__unserialize(unserialize($serialized)); } - /** - * @return string - */ - public function getPath() + public function getPath(): string { return $this->path; } @@ -122,7 +115,7 @@ public function getPath() /** * @return $this */ - public function setPath(string $pattern) + public function setPath(string $pattern): static { $pattern = $this->extractInlineDefaultsAndRequirements($pattern); @@ -134,10 +127,7 @@ public function setPath(string $pattern) return $this; } - /** - * @return string - */ - public function getHost() + public function getHost(): string { return $this->host; } @@ -145,7 +135,7 @@ public function getHost() /** * @return $this */ - public function setHost(?string $pattern) + public function setHost(?string $pattern): static { $this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern); $this->compiled = null; @@ -159,7 +149,7 @@ public function setHost(?string $pattern) * * @return string[] */ - public function getSchemes() + public function getSchemes(): array { return $this->schemes; } @@ -172,7 +162,7 @@ public function getSchemes() * * @return $this */ - public function setSchemes($schemes) + public function setSchemes(string|array $schemes): static { $this->schemes = array_map('strtolower', (array) $schemes); $this->compiled = null; @@ -182,10 +172,8 @@ public function setSchemes($schemes) /** * Checks if a scheme requirement has been set. - * - * @return bool */ - public function hasScheme(string $scheme) + public function hasScheme(string $scheme): bool { return \in_array(strtolower($scheme), $this->schemes, true); } @@ -196,7 +184,7 @@ public function hasScheme(string $scheme) * * @return string[] */ - public function getMethods() + public function getMethods(): array { return $this->methods; } @@ -209,7 +197,7 @@ public function getMethods() * * @return $this */ - public function setMethods($methods) + public function setMethods(string|array $methods): static { $this->methods = array_map('strtoupper', (array) $methods); $this->compiled = null; @@ -217,10 +205,7 @@ public function setMethods($methods) return $this; } - /** - * @return array - */ - public function getOptions() + public function getOptions(): array { return $this->options; } @@ -228,10 +213,10 @@ public function getOptions() /** * @return $this */ - public function setOptions(array $options) + public function setOptions(array $options): static { $this->options = [ - 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', + 'compiler_class' => RouteCompiler::class, ]; return $this->addOptions($options); @@ -240,7 +225,7 @@ public function setOptions(array $options) /** * @return $this */ - public function addOptions(array $options) + public function addOptions(array $options): static { foreach ($options as $name => $option) { $this->options[$name] = $option; @@ -253,11 +238,9 @@ public function addOptions(array $options) /** * Sets an option value. * - * @param mixed $value The option value - * * @return $this */ - public function setOption(string $name, $value) + public function setOption(string $name, mixed $value): static { $this->options[$name] = $value; $this->compiled = null; @@ -267,26 +250,18 @@ public function setOption(string $name, $value) /** * Returns the option value or null when not found. - * - * @return mixed */ - public function getOption(string $name) + public function getOption(string $name): mixed { return $this->options[$name] ?? null; } - /** - * @return bool - */ - public function hasOption(string $name) + public function hasOption(string $name): bool { return \array_key_exists($name, $this->options); } - /** - * @return array - */ - public function getDefaults() + public function getDefaults(): array { return $this->defaults; } @@ -294,7 +269,7 @@ public function getDefaults() /** * @return $this */ - public function setDefaults(array $defaults) + public function setDefaults(array $defaults): static { $this->defaults = []; @@ -304,7 +279,7 @@ public function setDefaults(array $defaults) /** * @return $this */ - public function addDefaults(array $defaults) + public function addDefaults(array $defaults): static { if (isset($defaults['_locale']) && $this->isLocalized()) { unset($defaults['_locale']); @@ -318,30 +293,20 @@ public function addDefaults(array $defaults) return $this; } - /** - * @return mixed - */ - public function getDefault(string $name) + public function getDefault(string $name): mixed { return $this->defaults[$name] ?? null; } - /** - * @return bool - */ - public function hasDefault(string $name) + public function hasDefault(string $name): bool { return \array_key_exists($name, $this->defaults); } /** - * Sets a default value. - * - * @param mixed $default The default value - * * @return $this */ - public function setDefault(string $name, $default) + public function setDefault(string $name, mixed $default): static { if ('_locale' === $name && $this->isLocalized()) { return $this; @@ -353,10 +318,7 @@ public function setDefault(string $name, $default) return $this; } - /** - * @return array - */ - public function getRequirements() + public function getRequirements(): array { return $this->requirements; } @@ -364,7 +326,7 @@ public function getRequirements() /** * @return $this */ - public function setRequirements(array $requirements) + public function setRequirements(array $requirements): static { $this->requirements = []; @@ -374,7 +336,7 @@ public function setRequirements(array $requirements) /** * @return $this */ - public function addRequirements(array $requirements) + public function addRequirements(array $requirements): static { if (isset($requirements['_locale']) && $this->isLocalized()) { unset($requirements['_locale']); @@ -388,18 +350,12 @@ public function addRequirements(array $requirements) return $this; } - /** - * @return string|null - */ - public function getRequirement(string $key) + public function getRequirement(string $key): ?string { return $this->requirements[$key] ?? null; } - /** - * @return bool - */ - public function hasRequirement(string $key) + public function hasRequirement(string $key): bool { return \array_key_exists($key, $this->requirements); } @@ -407,7 +363,7 @@ public function hasRequirement(string $key) /** * @return $this */ - public function setRequirement(string $key, string $regex) + public function setRequirement(string $key, string $regex): static { if ('_locale' === $key && $this->isLocalized()) { return $this; @@ -419,10 +375,7 @@ public function setRequirement(string $key, string $regex) return $this; } - /** - * @return string - */ - public function getCondition() + public function getCondition(): string { return $this->condition; } @@ -430,7 +383,7 @@ public function getCondition() /** * @return $this */ - public function setCondition(?string $condition) + public function setCondition(?string $condition): static { $this->condition = (string) $condition; $this->compiled = null; @@ -441,14 +394,12 @@ public function setCondition(?string $condition) /** * Compiles the route. * - * @return CompiledRoute - * * @throws \LogicException If the Route cannot be compiled because the * path or host pattern is invalid * * @see RouteCompiler which is responsible for the compilation process */ - public function compile() + public function compile(): CompiledRoute { if (null !== $this->compiled) { return $this->compiled; @@ -465,7 +416,7 @@ private function extractInlineDefaultsAndRequirements(string $pattern): string return $pattern; } - return preg_replace_callback('#\{(!?)(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) { + return preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) { if (isset($m[4][0])) { $this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null); } @@ -477,12 +428,12 @@ private function extractInlineDefaultsAndRequirements(string $pattern): string }, $pattern); } - private function sanitizeRequirement(string $key, string $regex) + private function sanitizeRequirement(string $key, string $regex): string { if ('' !== $regex) { if ('^' === $regex[0]) { $regex = substr($regex, 1); - } elseif (0 === strpos($regex, '\\A')) { + } elseif (str_starts_with($regex, '\\A')) { $regex = substr($regex, 2); } } diff --git a/symfony/routing/RouteCollection.php b/symfony/routing/RouteCollection.php index a0700bba3..2f49ad217 100644 --- a/symfony/routing/RouteCollection.php +++ b/symfony/routing/RouteCollection.php @@ -32,22 +32,22 @@ class RouteCollection implements \IteratorAggregate, \Countable /** * @var array */ - private $routes = []; + private array $routes = []; /** * @var array */ - private $aliases = []; + private array $aliases = []; /** * @var array */ - private $resources = []; + private array $resources = []; /** * @var array */ - private $priorities = []; + private array $priorities = []; public function __clone() { @@ -69,37 +69,29 @@ public function __clone() * * @return \ArrayIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->all()); } /** * Gets the number of Routes in this collection. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->routes); } /** - * @param int $priority + * @return void */ - public function add(string $name, Route $route/* , int $priority = 0 */) + public function add(string $name, Route $route, int $priority = 0) { - if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { - trigger_deprecation('symfony/routing', '5.1', 'The "%s()" method will have a new "int $priority = 0" argument in version 6.0, not defining it is deprecated.', __METHOD__); - } - unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); $this->routes[$name] = $route; - if ($priority = 3 <= \func_num_args() ? func_get_arg(2) : 0) { + if ($priority) { $this->priorities[$name] = $priority; } } @@ -109,14 +101,12 @@ public function add(string $name, Route $route/* , int $priority = 0 */) * * @return array */ - public function all() + public function all(): array { if ($this->priorities) { $priorities = $this->priorities; $keysOrder = array_flip(array_keys($this->routes)); - uksort($this->routes, static function ($n1, $n2) use ($priorities, $keysOrder) { - return (($priorities[$n2] ?? 0) <=> ($priorities[$n1] ?? 0)) ?: ($keysOrder[$n1] <=> $keysOrder[$n2]); - }); + uksort($this->routes, static fn ($n1, $n2) => (($priorities[$n2] ?? 0) <=> ($priorities[$n1] ?? 0)) ?: ($keysOrder[$n1] <=> $keysOrder[$n2])); } return $this->routes; @@ -124,10 +114,8 @@ public function all() /** * Gets a route by name. - * - * @return Route|null */ - public function get(string $name) + public function get(string $name): ?Route { $visited = []; while (null !== $alias = $this->aliases[$name] ?? null) { @@ -154,17 +142,36 @@ public function get(string $name) * Removes a route or an array of routes by name from the collection. * * @param string|string[] $name The route name or an array of route names + * + * @return void */ - public function remove($name) + public function remove(string|array $name) { + $routes = []; foreach ((array) $name as $n) { + if (isset($this->routes[$n])) { + $routes[] = $n; + } + unset($this->routes[$n], $this->priorities[$n], $this->aliases[$n]); } + + if (!$routes) { + return; + } + + foreach ($this->aliases as $k => $alias) { + if (\in_array($alias->getId(), $routes, true)) { + unset($this->aliases[$k]); + } + } } /** * Adds a route collection at the end of the current set by appending all * routes of the added collection. + * + * @return void */ public function addCollection(self $collection) { @@ -192,6 +199,8 @@ public function addCollection(self $collection) /** * Adds a prefix to the path of all child routes. + * + * @return void */ public function addPrefix(string $prefix, array $defaults = [], array $requirements = []) { @@ -210,6 +219,8 @@ public function addPrefix(string $prefix, array $defaults = [], array $requireme /** * Adds a prefix to the name of all the routes within in the collection. + * + * @return void */ public function addNamePrefix(string $prefix) { @@ -238,6 +249,8 @@ public function addNamePrefix(string $prefix) /** * Sets the host pattern on all routes. + * + * @return void */ public function setHost(?string $pattern, array $defaults = [], array $requirements = []) { @@ -252,6 +265,8 @@ public function setHost(?string $pattern, array $defaults = [], array $requireme * Sets a condition on all routes. * * Existing conditions will be overridden. + * + * @return void */ public function setCondition(?string $condition) { @@ -264,6 +279,8 @@ public function setCondition(?string $condition) * Adds defaults to all routes. * * An existing default value under the same name in a route will be overridden. + * + * @return void */ public function addDefaults(array $defaults) { @@ -278,6 +295,8 @@ public function addDefaults(array $defaults) * Adds requirements to all routes. * * An existing requirement under the same name in a route will be overridden. + * + * @return void */ public function addRequirements(array $requirements) { @@ -292,6 +311,8 @@ public function addRequirements(array $requirements) * Adds options to all routes. * * An existing option value under the same name in a route will be overridden. + * + * @return void */ public function addOptions(array $options) { @@ -306,8 +327,10 @@ public function addOptions(array $options) * Sets the schemes (e.g. 'https') all child routes are restricted to. * * @param string|string[] $schemes The scheme or an array of schemes + * + * @return void */ - public function setSchemes($schemes) + public function setSchemes(string|array $schemes) { foreach ($this->routes as $route) { $route->setSchemes($schemes); @@ -318,8 +341,10 @@ public function setSchemes($schemes) * Sets the HTTP methods (e.g. 'POST') all child routes are restricted to. * * @param string|string[] $methods The method or an array of methods + * + * @return void */ - public function setMethods($methods) + public function setMethods(string|array $methods) { foreach ($this->routes as $route) { $route->setMethods($methods); @@ -331,7 +356,7 @@ public function setMethods($methods) * * @return ResourceInterface[] */ - public function getResources() + public function getResources(): array { return array_values($this->resources); } @@ -339,6 +364,8 @@ public function getResources() /** * Adds a resource for this collection. If the resource already exists * it is not added. + * + * @return void */ public function addResource(ResourceInterface $resource) { @@ -380,4 +407,9 @@ public function getAlias(string $name): ?Alias { return $this->aliases[$name] ?? null; } + + public function getPriority(string $name): ?int + { + return $this->priorities[$name] ?? null; + } } diff --git a/symfony/routing/RouteCollectionBuilder.php b/symfony/routing/RouteCollectionBuilder.php deleted file mode 100644 index d7eed31eb..000000000 --- a/symfony/routing/RouteCollectionBuilder.php +++ /dev/null @@ -1,364 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Routing; - -use Symfony\Component\Config\Exception\LoaderLoadException; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Resource\ResourceInterface; -use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - -trigger_deprecation('symfony/routing', '5.1', 'The "%s" class is deprecated, use "%s" instead.', RouteCollectionBuilder::class, RoutingConfigurator::class); - -/** - * Helps add and import routes into a RouteCollection. - * - * @author Ryan Weaver - * - * @deprecated since Symfony 5.1, use RoutingConfigurator instead - */ -class RouteCollectionBuilder -{ - /** - * @var Route[]|RouteCollectionBuilder[] - */ - private $routes = []; - - private $loader; - private $defaults = []; - private $prefix; - private $host; - private $condition; - private $requirements = []; - private $options = []; - private $schemes; - private $methods; - private $resources = []; - - public function __construct(LoaderInterface $loader = null) - { - $this->loader = $loader; - } - - /** - * Import an external routing resource and returns the RouteCollectionBuilder. - * - * $routes->import('blog.yml', '/blog'); - * - * @param mixed $resource - * - * @return self - * - * @throws LoaderLoadException - */ - public function import($resource, string $prefix = '/', string $type = null) - { - /** @var RouteCollection[] $collections */ - $collections = $this->load($resource, $type); - - // create a builder from the RouteCollection - $builder = $this->createBuilder(); - - foreach ($collections as $collection) { - if (null === $collection) { - continue; - } - - foreach ($collection->all() as $name => $route) { - $builder->addRoute($route, $name); - } - - foreach ($collection->getResources() as $resource) { - $builder->addResource($resource); - } - } - - // mount into this builder - $this->mount($prefix, $builder); - - return $builder; - } - - /** - * Adds a route and returns it for future modification. - * - * @return Route - */ - public function add(string $path, string $controller, string $name = null) - { - $route = new Route($path); - $route->setDefault('_controller', $controller); - $this->addRoute($route, $name); - - return $route; - } - - /** - * Returns a RouteCollectionBuilder that can be configured and then added with mount(). - * - * @return self - */ - public function createBuilder() - { - return new self($this->loader); - } - - /** - * Add a RouteCollectionBuilder. - */ - public function mount(string $prefix, self $builder) - { - $builder->prefix = trim(trim($prefix), '/'); - $this->routes[] = $builder; - } - - /** - * Adds a Route object to the builder. - * - * @return $this - */ - public function addRoute(Route $route, string $name = null) - { - if (null === $name) { - // used as a flag to know which routes will need a name later - $name = '_unnamed_route_'.spl_object_hash($route); - } - - $this->routes[$name] = $route; - - return $this; - } - - /** - * Sets the host on all embedded routes (unless already set). - * - * @return $this - */ - public function setHost(?string $pattern) - { - $this->host = $pattern; - - return $this; - } - - /** - * Sets a condition on all embedded routes (unless already set). - * - * @return $this - */ - public function setCondition(?string $condition) - { - $this->condition = $condition; - - return $this; - } - - /** - * Sets a default value that will be added to all embedded routes (unless that - * default value is already set). - * - * @param mixed $value - * - * @return $this - */ - public function setDefault(string $key, $value) - { - $this->defaults[$key] = $value; - - return $this; - } - - /** - * Sets a requirement that will be added to all embedded routes (unless that - * requirement is already set). - * - * @param mixed $regex - * - * @return $this - */ - public function setRequirement(string $key, $regex) - { - $this->requirements[$key] = $regex; - - return $this; - } - - /** - * Sets an option that will be added to all embedded routes (unless that - * option is already set). - * - * @param mixed $value - * - * @return $this - */ - public function setOption(string $key, $value) - { - $this->options[$key] = $value; - - return $this; - } - - /** - * Sets the schemes on all embedded routes (unless already set). - * - * @param array|string $schemes - * - * @return $this - */ - public function setSchemes($schemes) - { - $this->schemes = $schemes; - - return $this; - } - - /** - * Sets the methods on all embedded routes (unless already set). - * - * @param array|string $methods - * - * @return $this - */ - public function setMethods($methods) - { - $this->methods = $methods; - - return $this; - } - - /** - * Adds a resource for this collection. - * - * @return $this - */ - private function addResource(ResourceInterface $resource): self - { - $this->resources[] = $resource; - - return $this; - } - - /** - * Creates the final RouteCollection and returns it. - * - * @return RouteCollection - */ - public function build() - { - $routeCollection = new RouteCollection(); - - foreach ($this->routes as $name => $route) { - if ($route instanceof Route) { - $route->setDefaults(array_merge($this->defaults, $route->getDefaults())); - $route->setOptions(array_merge($this->options, $route->getOptions())); - - foreach ($this->requirements as $key => $val) { - if (!$route->hasRequirement($key)) { - $route->setRequirement($key, $val); - } - } - - if (null !== $this->prefix) { - $route->setPath('/'.$this->prefix.$route->getPath()); - } - - if (!$route->getHost()) { - $route->setHost($this->host); - } - - if (!$route->getCondition()) { - $route->setCondition($this->condition); - } - - if (!$route->getSchemes()) { - $route->setSchemes($this->schemes); - } - - if (!$route->getMethods()) { - $route->setMethods($this->methods); - } - - // auto-generate the route name if it's been marked - if ('_unnamed_route_' === substr($name, 0, 15)) { - $name = $this->generateRouteName($route); - } - - $routeCollection->add($name, $route); - } else { - /* @var self $route */ - $subCollection = $route->build(); - if (null !== $this->prefix) { - $subCollection->addPrefix($this->prefix); - } - - $routeCollection->addCollection($subCollection); - } - } - - foreach ($this->resources as $resource) { - $routeCollection->addResource($resource); - } - - return $routeCollection; - } - - /** - * Generates a route name based on details of this route. - */ - private function generateRouteName(Route $route): string - { - $methods = implode('_', $route->getMethods()).'_'; - - $routeName = $methods.$route->getPath(); - $routeName = str_replace(['/', ':', '|', '-'], '_', $routeName); - $routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName); - - // Collapse consecutive underscores down into a single underscore. - $routeName = preg_replace('/_+/', '_', $routeName); - - return $routeName; - } - - /** - * Finds a loader able to load an imported resource and loads it. - * - * @param mixed $resource A resource - * @param string|null $type The resource type or null if unknown - * - * @return RouteCollection[] - * - * @throws LoaderLoadException If no loader is found - */ - private function load($resource, string $type = null): array - { - if (null === $this->loader) { - throw new \BadMethodCallException('Cannot import other routing resources: you must pass a LoaderInterface when constructing RouteCollectionBuilder.'); - } - - if ($this->loader->supports($resource, $type)) { - $collections = $this->loader->load($resource, $type); - - return \is_array($collections) ? $collections : [$collections]; - } - - if (null === $resolver = $this->loader->getResolver()) { - throw new LoaderLoadException($resource, null, 0, null, $type); - } - - if (false === $loader = $resolver->resolve($resource, $type)) { - throw new LoaderLoadException($resource, null, 0, null, $type); - } - - $collections = $loader->load($resource, $type); - - return \is_array($collections) ? $collections : [$collections]; - } -} diff --git a/symfony/routing/RouteCompiler.php b/symfony/routing/RouteCompiler.php index 7e78c2931..330639f48 100644 --- a/symfony/routing/RouteCompiler.php +++ b/symfony/routing/RouteCompiler.php @@ -19,11 +19,6 @@ */ class RouteCompiler implements RouteCompilerInterface { - /** - * @deprecated since Symfony 5.1, to be removed in 6.0 - */ - public const REGEX_DELIMITER = '#'; - /** * This string defines the characters that are automatically considered separators in front of * optional placeholders (with default and no static text following). Such a single separator @@ -40,14 +35,12 @@ class RouteCompiler implements RouteCompilerInterface public const VARIABLE_MAXIMUM_LENGTH = 32; /** - * {@inheritdoc} - * * @throws \InvalidArgumentException if a path variable is named _fragment * @throws \LogicException if a variable is referenced more than once * @throws \DomainException if a variable name starts with a digit or if it is too long to be successfully used as * a PCRE subpattern */ - public static function compile(Route $route) + public static function compile(Route $route): CompiledRoute { $hostVariables = []; $variables = []; @@ -122,7 +115,7 @@ private static function compilePattern(Route $route, string $pattern, bool $isHo // Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable // in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself. - preg_match_all('#\{(!)?(\w+)\}#', $pattern, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER); + preg_match_all('#\{(!)?([\w\x80-\xFF]+)\}#', $pattern, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER); foreach ($matches as $match) { $important = $match[1][1] >= 0; $varName = $match[2][0]; @@ -175,7 +168,7 @@ private static function compilePattern(Route $route, string $pattern, bool $isHo preg_quote($defaultSeparator), $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator) : '' ); - if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) { + if (('' !== $nextSeparator && !preg_match('#^\{[\w\x80-\xFF]+\}#', $followingPattern)) || '' === $followingPattern) { // When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive // quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns. // Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow @@ -276,7 +269,7 @@ private static function findNextSeparator(string $pattern, bool $useUtf8): strin return ''; } // first remove all placeholders from the pattern so we can find the next real static character - if ('' === $pattern = preg_replace('#\{\w+\}#', '', $pattern)) { + if ('' === $pattern = preg_replace('#\{[\w\x80-\xFF]+\}#', '', $pattern)) { return ''; } if ($useUtf8) { diff --git a/symfony/routing/RouteCompilerInterface.php b/symfony/routing/RouteCompilerInterface.php index 9bae33a91..621561176 100644 --- a/symfony/routing/RouteCompilerInterface.php +++ b/symfony/routing/RouteCompilerInterface.php @@ -21,10 +21,8 @@ interface RouteCompilerInterface /** * Compiles the current route instance. * - * @return CompiledRoute - * * @throws \LogicException If the Route cannot be compiled because the * path or host pattern is invalid */ - public static function compile(Route $route); + public static function compile(Route $route): CompiledRoute; } diff --git a/symfony/routing/Router.php b/symfony/routing/Router.php index 25b9456af..b769caee6 100644 --- a/symfony/routing/Router.php +++ b/symfony/routing/Router.php @@ -82,22 +82,16 @@ class Router implements RouterInterface, RequestMatcherInterface */ protected $defaultLocale; - /** - * @var ConfigCacheFactoryInterface|null - */ - private $configCacheFactory; + private ConfigCacheFactoryInterface $configCacheFactory; /** * @var ExpressionFunctionProviderInterface[] */ - private $expressionLanguageProviders = []; + private array $expressionLanguageProviders = []; - private static $cache = []; + private static ?array $cache = []; - /** - * @param mixed $resource The main resource to load - */ - public function __construct(LoaderInterface $loader, $resource, array $options = [], RequestContext $context = null, LoggerInterface $logger = null, string $defaultLocale = null) + public function __construct(LoaderInterface $loader, mixed $resource, array $options = [], ?RequestContext $context = null, ?LoggerInterface $logger = null, ?string $defaultLocale = null) { $this->loader = $loader; $this->resource = $resource; @@ -122,6 +116,8 @@ public function __construct(LoaderInterface $loader, $resource, array $options = * * strict_requirements: Configure strict requirement checking for generators * implementing ConfigurableRequirementsInterface (default is true) * + * @return void + * * @throws \InvalidArgumentException When unsupported option is provided */ public function setOptions(array $options) @@ -155,11 +151,11 @@ public function setOptions(array $options) /** * Sets an option. * - * @param mixed $value The value + * @return void * * @throws \InvalidArgumentException */ - public function setOption(string $key, $value) + public function setOption(string $key, mixed $value) { if (!\array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); @@ -171,11 +167,9 @@ public function setOption(string $key, $value) /** * Gets an option value. * - * @return mixed - * * @throws \InvalidArgumentException */ - public function getOption(string $key) + public function getOption(string $key): mixed { if (!\array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); @@ -185,68 +179,54 @@ public function getOption(string $key) } /** - * {@inheritdoc} + * @return RouteCollection */ public function getRouteCollection() { - if (null === $this->collection) { - $this->collection = $this->loader->load($this->resource, $this->options['resource_type']); - } - - return $this->collection; + return $this->collection ??= $this->loader->load($this->resource, $this->options['resource_type']); } /** - * {@inheritdoc} + * @return void */ public function setContext(RequestContext $context) { $this->context = $context; - if (null !== $this->matcher) { + if (isset($this->matcher)) { $this->getMatcher()->setContext($context); } - if (null !== $this->generator) { + if (isset($this->generator)) { $this->getGenerator()->setContext($context); } } - /** - * {@inheritdoc} - */ - public function getContext() + public function getContext(): RequestContext { return $this->context; } /** * Sets the ConfigCache factory to use. + * + * @return void */ public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) { $this->configCacheFactory = $configCacheFactory; } - /** - * {@inheritdoc} - */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH) + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { return $this->getGenerator()->generate($name, $parameters, $referenceType); } - /** - * {@inheritdoc} - */ - public function match(string $pathinfo) + public function match(string $pathinfo): array { return $this->getMatcher()->match($pathinfo); } - /** - * {@inheritdoc} - */ - public function matchRequest(Request $request) + public function matchRequest(Request $request): array { $matcher = $this->getMatcher(); if (!$matcher instanceof RequestMatcherInterface) { @@ -259,12 +239,10 @@ public function matchRequest(Request $request) /** * Gets the UrlMatcher or RequestMatcher instance associated with this Router. - * - * @return UrlMatcherInterface|RequestMatcherInterface */ - public function getMatcher() + public function getMatcher(): UrlMatcherInterface|RequestMatcherInterface { - if (null !== $this->matcher) { + if (isset($this->matcher)) { return $this->matcher; } @@ -294,6 +272,7 @@ function (ConfigCacheInterface $cache) { } $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); + unset(self::$cache[$cache->getPath()]); } ); @@ -302,12 +281,10 @@ function (ConfigCacheInterface $cache) { /** * Gets the UrlGenerator instance associated with this Router. - * - * @return UrlGeneratorInterface */ - public function getGenerator() + public function getGenerator(): UrlGeneratorInterface { - if (null !== $this->generator) { + if (isset($this->generator)) { return $this->generator; } @@ -325,6 +302,7 @@ function (ConfigCacheInterface $cache) { $dumper = $this->getGeneratorDumperInstance(); $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); + unset(self::$cache[$cache->getPath()]); } ); @@ -338,23 +316,20 @@ function (ConfigCacheInterface $cache) { return $this->generator; } + /** + * @return void + */ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; } - /** - * @return GeneratorDumperInterface - */ - protected function getGeneratorDumperInstance() + protected function getGeneratorDumperInstance(): GeneratorDumperInterface { return new $this->options['generator_dumper_class']($this->getRouteCollection()); } - /** - * @return MatcherDumperInterface - */ - protected function getMatcherDumperInstance() + protected function getMatcherDumperInstance(): MatcherDumperInterface { return new $this->options['matcher_dumper_class']($this->getRouteCollection()); } @@ -365,16 +340,12 @@ protected function getMatcherDumperInstance() */ private function getConfigCacheFactory(): ConfigCacheFactoryInterface { - if (null === $this->configCacheFactory) { - $this->configCacheFactory = new ConfigCacheFactory($this->options['debug']); - } - - return $this->configCacheFactory; + return $this->configCacheFactory ??= new ConfigCacheFactory($this->options['debug']); } private static function getCompiledRoutes(string $path): array { - if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) { + if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL))) { self::$cache = null; } @@ -382,10 +353,6 @@ private static function getCompiledRoutes(string $path): array return require $path; } - if (isset(self::$cache[$path])) { - return self::$cache[$path]; - } - - return self::$cache[$path] = require $path; + return self::$cache[$path] ??= require $path; } } diff --git a/symfony/service-contracts/Attribute/SubscribedService.php b/symfony/service-contracts/Attribute/SubscribedService.php index 10d1bc38e..f850b8401 100644 --- a/symfony/service-contracts/Attribute/SubscribedService.php +++ b/symfony/service-contracts/Attribute/SubscribedService.php @@ -11,10 +11,15 @@ namespace Symfony\Contracts\Service\Attribute; -use Symfony\Contracts\Service\ServiceSubscriberTrait; +use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; +use Symfony\Contracts\Service\ServiceSubscriberInterface; /** - * Use with {@see ServiceSubscriberTrait} to mark a method's return type + * For use as the return value for {@see ServiceSubscriberInterface}. + * + * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi')) + * + * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type * as a subscribed service. * * @author Kevin Bond @@ -22,12 +27,21 @@ #[\Attribute(\Attribute::TARGET_METHOD)] final class SubscribedService { + /** @var object[] */ + public array $attributes; + /** - * @param string|null $key The key to use for the service - * If null, use "ClassName::methodName" + * @param string|null $key The key to use for the service + * @param class-string|null $type The service class + * @param bool $nullable Whether the service is optional + * @param object|object[] $attributes One or more dependency injection attributes to use */ public function __construct( - public ?string $key = null + public ?string $key = null, + public ?string $type = null, + public bool $nullable = false, + array|object $attributes = [], ) { + $this->attributes = \is_array($attributes) ? $attributes : [$attributes]; } } diff --git a/symfony/service-contracts/LICENSE b/symfony/service-contracts/LICENSE index 74cdc2dbf..7536caeae 100644 --- a/symfony/service-contracts/LICENSE +++ b/symfony/service-contracts/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2022 Fabien Potencier +Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/service-contracts/ResetInterface.php b/symfony/service-contracts/ResetInterface.php index 1af1075ee..a4f389b01 100644 --- a/symfony/service-contracts/ResetInterface.php +++ b/symfony/service-contracts/ResetInterface.php @@ -26,5 +26,8 @@ */ interface ResetInterface { + /** + * @return void + */ public function reset(); } diff --git a/symfony/service-contracts/ServiceCollectionInterface.php b/symfony/service-contracts/ServiceCollectionInterface.php new file mode 100644 index 000000000..2333139ce --- /dev/null +++ b/symfony/service-contracts/ServiceCollectionInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceProviderInterface that is also countable and iterable. + * + * @author Kevin Bond + * + * @template-covariant T of mixed + * + * @extends ServiceProviderInterface + * @extends \IteratorAggregate + */ +interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate +{ +} diff --git a/symfony/service-contracts/ServiceLocatorTrait.php b/symfony/service-contracts/ServiceLocatorTrait.php index 19d3e80ff..b62ec3e53 100644 --- a/symfony/service-contracts/ServiceLocatorTrait.php +++ b/symfony/service-contracts/ServiceLocatorTrait.php @@ -31,24 +31,18 @@ trait ServiceLocatorTrait private array $providedTypes; /** - * @param callable[] $factories + * @param array $factories */ public function __construct(array $factories) { $this->factories = $factories; } - /** - * {@inheritdoc} - */ public function has(string $id): bool { return isset($this->factories[$id]); } - /** - * {@inheritdoc} - */ public function get(string $id): mixed { if (!isset($this->factories[$id])) { @@ -71,9 +65,6 @@ public function get(string $id): mixed } } - /** - * {@inheritdoc} - */ public function getProvidedServices(): array { if (!isset($this->providedTypes)) { diff --git a/symfony/service-contracts/ServiceMethodsSubscriberTrait.php b/symfony/service-contracts/ServiceMethodsSubscriberTrait.php new file mode 100644 index 000000000..0d89d9f25 --- /dev/null +++ b/symfony/service-contracts/ServiceMethodsSubscriberTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @author Kevin Bond + */ +trait ServiceMethodsSubscriberTrait +{ + protected ContainerInterface $container; + + public static function getSubscribedServices(): array + { + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } + + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } + + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $returnType->allowsNull(); + + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; + } + } + + return $services; + } + + #[Required] + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + $ret = null; + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { + $ret = parent::setContainer($container); + } + + $this->container = $container; + + return $ret; + } +} diff --git a/symfony/service-contracts/ServiceProviderInterface.php b/symfony/service-contracts/ServiceProviderInterface.php index c60ad0bd4..2e71f00c6 100644 --- a/symfony/service-contracts/ServiceProviderInterface.php +++ b/symfony/service-contracts/ServiceProviderInterface.php @@ -18,9 +18,18 @@ * * @author Nicolas Grekas * @author Mateusz Sip + * + * @template-covariant T of mixed */ interface ServiceProviderInterface extends ContainerInterface { + /** + * @return T + */ + public function get(string $id): mixed; + + public function has(string $id): bool; + /** * Returns an associative array of service types keyed by the identifiers provided by the current container. * @@ -30,7 +39,7 @@ interface ServiceProviderInterface extends ContainerInterface * * ['foo' => '?'] means the container provides service name "foo" of unspecified type * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null * - * @return string[] The provided service types, keyed by service names + * @return array The provided service types, keyed by service names */ public function getProvidedServices(): array; } diff --git a/symfony/service-contracts/ServiceSubscriberInterface.php b/symfony/service-contracts/ServiceSubscriberInterface.php index 881ab971a..3da19169b 100644 --- a/symfony/service-contracts/ServiceSubscriberInterface.php +++ b/symfony/service-contracts/ServiceSubscriberInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Contracts\Service; +use Symfony\Contracts\Service\Attribute\SubscribedService; + /** * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. * @@ -29,7 +31,8 @@ interface ServiceSubscriberInterface { /** - * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * Returns an array of service types (or {@see SubscribedService} objects) required + * by such instances, optionally keyed by the service names used internally. * * For mandatory dependencies: * @@ -47,7 +50,13 @@ interface ServiceSubscriberInterface * * ['?Psr\Log\LoggerInterface'] is a shortcut for * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] * - * @return string[] The required service types, optionally keyed by service names + * additionally, an array of {@see SubscribedService}'s can be returned: + * + * * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)] + * * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)] + * * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))] + * + * @return string[]|SubscribedService[] The required service types, optionally keyed by service names */ public static function getSubscribedServices(): array; } diff --git a/symfony/service-contracts/ServiceSubscriberTrait.php b/symfony/service-contracts/ServiceSubscriberTrait.php index ee9d9d9dd..cc3bc321a 100644 --- a/symfony/service-contracts/ServiceSubscriberTrait.php +++ b/symfony/service-contracts/ServiceSubscriberTrait.php @@ -12,22 +12,26 @@ namespace Symfony\Contracts\Service; use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; +trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class); + /** - * Implementation of ServiceSubscriberInterface that determines subscribed services from - * method return types. Service ids are available as "ClassName::methodName". + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @property ContainerInterface $container * * @author Kevin Bond + * + * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead */ trait ServiceSubscriberTrait { - /** @var ContainerInterface */ - protected $container; - - /** - * {@inheritdoc} - */ public static function getSubscribedServices(): array { $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; @@ -49,29 +53,32 @@ public static function getSubscribedServices(): array throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); } - $serviceId = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $returnType->allowsNull(); - if ($returnType->allowsNull()) { - $serviceId = '?'.$serviceId; + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; } - - $services[$attribute->newInstance()->key ?? self::class.'::'.$method->name] = $serviceId; } return $services; } - /** - * @required - */ + #[Required] public function setContainer(ContainerInterface $container): ?ContainerInterface { - $this->container = $container; - + $ret = null; if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { - return parent::setContainer($container); + $ret = parent::setContainer($container); } - return null; + $this->container = $container; + + return $ret; } }