diff --git a/composer.json b/composer.json index 6d23d37a7..2e18d76d1 100644 --- a/composer.json +++ b/composer.json @@ -51,13 +51,13 @@ "symfony/console": "^5.4.24", "symfony/event-dispatcher": "4.4.30", "symfony/event-dispatcher-contracts": "1.1.9", - "symfony/http-foundation": "^5.4.10", + "symfony/http-foundation": "^5.4.24", "symfony/mailer": "^5.4.13", "symfony/polyfill-intl-grapheme": "^1.20", "symfony/polyfill-intl-normalizer": "^1.20", "symfony/process": "^5.4.24", - "symfony/routing": "4.4.30", - "symfony/translation": "^4.4.41", + "symfony/routing": "^5.4.24", + "symfony/translation": "^5.4.24", "wapmorgan/mp3info": "^0.1.0", "web-auth/webauthn-lib": "^3.1" }, diff --git a/composer.lock b/composer.lock index 4650eb1cb..2a42c4704 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": "b5b1750517abfc031e6543455651776f", + "content-hash": "b67e6a271daa5ecb2f361e80ebc42704", "packages": [ { "name": "aws/aws-crt-php", @@ -4902,16 +4902,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.4.10", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e" + "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e7793b7906f72a8cc51054fbca9dcff7a8af1c1e", - "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f66be2706075c5f6325d2fe2b743a57fb5d23f6b", + "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b", "shasum": "" }, "require": { @@ -4923,8 +4923,11 @@ "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/mime": "^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" }, "suggest": { "symfony/mime": "To use the file extension guesser" @@ -4955,7 +4958,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.10" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.25" }, "funding": [ { @@ -4971,7 +4974,7 @@ "type": "tidelift" } ], - "time": "2022-06-19T13:13:40+00:00" + "time": "2023-06-22T08:06:06+00:00" }, { "name": "symfony/mailer", @@ -5849,38 +5852,39 @@ }, { "name": "symfony/routing", - "version": "v4.4.30", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e" + "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/9ddf033927ad9f30ba2bfd167a7b342cafa13e8e", - "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e", + "url": "https://api.github.com/repos/symfony/routing/zipball/56bfc1394f7011303eb2e22724f9b422d3f14649", + "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-php80": "^1.16" }, "conflict": { - "symfony/config": "<4.2", - "symfony/dependency-injection": "<3.4", - "symfony/yaml": "<3.4" + "doctrine/annotations": "<1.12", + "symfony/config": "<5.3", + "symfony/dependency-injection": "<4.4", + "symfony/yaml": "<4.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4", + "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0" + "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": { - "doctrine/annotations": "For using the annotation loader", "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", @@ -5918,7 +5922,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v4.4.30" + "source": "https://github.com/symfony/routing/tree/v5.4.25" }, "funding": [ { @@ -5934,7 +5938,7 @@ "type": "tidelift" } ], - "time": "2021-08-04T21:41:01+00:00" + "time": "2023-06-05T14:18:47+00:00" }, { "name": "symfony/service-contracts", @@ -6105,43 +6109,48 @@ }, { "name": "symfony/translation", - "version": "v4.4.41", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331" + "reference": "de237e59c5833422342be67402d487fbf50334ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", + "url": "https://api.github.com/repos/symfony/translation/zipball/de237e59c5833422342be67402d487fbf50334ff", + "reference": "de237e59c5833422342be67402d487fbf50334ff", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^1.1.6|^2" + "symfony/translation-contracts": "^2.3" }, "conflict": { - "symfony/config": "<3.4", - "symfony/dependency-injection": "<3.4", - "symfony/http-kernel": "<4.4", - "symfony/yaml": "<3.4" + "symfony/config": "<4.4", + "symfony/console": "<5.3", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" }, "provide": { - "symfony/translation-implementation": "1.0|2.0" + "symfony/translation-implementation": "2.3" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/finder": "~2.8|~3.0|~4.0|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/intl": "^3.4|^4.0|^5.0", - "symfony/service-contracts": "^1.1.2|^2", - "symfony/yaml": "^3.4|^4.0|^5.0" + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/http-client-contracts": "^1.1|^2.0|^3.0", + "symfony/http-kernel": "^5.0|^6.0", + "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/service-contracts": "^1.1.2|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" }, "suggest": { "psr/log-implementation": "To use logging capability in translator", @@ -6150,6 +6159,9 @@ }, "type": "library", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, @@ -6174,7 +6186,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v4.4.41" + "source": "https://github.com/symfony/translation/tree/v5.4.24" }, "funding": [ { @@ -6190,7 +6202,7 @@ "type": "tidelift" } ], - "time": "2022-04-21T07:22:34+00:00" + "time": "2023-05-19T12:34:17+00:00" }, { "name": "symfony/translation-contracts", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index 98e595529..237c89434 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -3121,40 +3121,45 @@ 'Symfony\\Component\\Process\\Pipes\\WindowsPipes' => $vendorDir . '/symfony/process/Pipes/WindowsPipes.php', 'Symfony\\Component\\Process\\Process' => $vendorDir . '/symfony/process/Process.php', '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\\CompiledRoute' => $vendorDir . '/symfony/routing/CompiledRoute.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', 'Symfony\\Component\\Routing\\Exception\\InvalidParameterException' => $vendorDir . '/symfony/routing/Exception/InvalidParameterException.php', 'Symfony\\Component\\Routing\\Exception\\MethodNotAllowedException' => $vendorDir . '/symfony/routing/Exception/MethodNotAllowedException.php', 'Symfony\\Component\\Routing\\Exception\\MissingMandatoryParametersException' => $vendorDir . '/symfony/routing/Exception/MissingMandatoryParametersException.php', 'Symfony\\Component\\Routing\\Exception\\NoConfigurationException' => $vendorDir . '/symfony/routing/Exception/NoConfigurationException.php', 'Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException' => $vendorDir . '/symfony/routing/Exception/ResourceNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RouteCircularReferenceException' => $vendorDir . '/symfony/routing/Exception/RouteCircularReferenceException.php', 'Symfony\\Component\\Routing\\Exception\\RouteNotFoundException' => $vendorDir . '/symfony/routing/Exception/RouteNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RuntimeException' => $vendorDir . '/symfony/routing/Exception/RuntimeException.php', 'Symfony\\Component\\Routing\\Generator\\CompiledUrlGenerator' => $vendorDir . '/symfony/routing/Generator/CompiledUrlGenerator.php', 'Symfony\\Component\\Routing\\Generator\\ConfigurableRequirementsInterface' => $vendorDir . '/symfony/routing/Generator/ConfigurableRequirementsInterface.php', 'Symfony\\Component\\Routing\\Generator\\Dumper\\CompiledUrlGeneratorDumper' => $vendorDir . '/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php', 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumper' => $vendorDir . '/symfony/routing/Generator/Dumper/GeneratorDumper.php', 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumperInterface' => $vendorDir . '/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php', - 'Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper' => $vendorDir . '/symfony/routing/Generator/Dumper/PhpGeneratorDumper.php', 'Symfony\\Component\\Routing\\Generator\\UrlGenerator' => $vendorDir . '/symfony/routing/Generator/UrlGenerator.php', 'Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface' => $vendorDir . '/symfony/routing/Generator/UrlGeneratorInterface.php', '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\\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', 'Symfony\\Component\\Routing\\Loader\\Configurator\\ImportConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/ImportConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\RouteConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/RouteConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/RoutingConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\AddTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/AddTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\HostTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/HostTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\LocalizedRouteTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\PrefixTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\RouteTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/RouteTrait.php', 'Symfony\\Component\\Routing\\Loader\\ContainerLoader' => $vendorDir . '/symfony/routing/Loader/ContainerLoader.php', - 'Symfony\\Component\\Routing\\Loader\\DependencyInjection\\ServiceRouterLoader' => $vendorDir . '/symfony/routing/Loader/DependencyInjection/ServiceRouterLoader.php', 'Symfony\\Component\\Routing\\Loader\\DirectoryLoader' => $vendorDir . '/symfony/routing/Loader/DirectoryLoader.php', '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\\ObjectRouteLoader' => $vendorDir . '/symfony/routing/Loader/ObjectRouteLoader.php', 'Symfony\\Component\\Routing\\Loader\\PhpFileLoader' => $vendorDir . '/symfony/routing/Loader/PhpFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\XmlFileLoader' => $vendorDir . '/symfony/routing/Loader/XmlFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\YamlFileLoader' => $vendorDir . '/symfony/routing/Loader/YamlFileLoader.php', @@ -3163,8 +3168,8 @@ 'Symfony\\Component\\Routing\\Matcher\\Dumper\\CompiledUrlMatcherTrait' => $vendorDir . '/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php', 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumper' => $vendorDir . '/symfony/routing/Matcher/Dumper/MatcherDumper.php', 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumperInterface' => $vendorDir . '/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php', - 'Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper' => $vendorDir . '/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php', 'Symfony\\Component\\Routing\\Matcher\\Dumper\\StaticPrefixCollection' => $vendorDir . '/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php', + 'Symfony\\Component\\Routing\\Matcher\\ExpressionLanguageProvider' => $vendorDir . '/symfony/routing/Matcher/ExpressionLanguageProvider.php', 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcher' => $vendorDir . '/symfony/routing/Matcher/RedirectableUrlMatcher.php', 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface' => $vendorDir . '/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php', 'Symfony\\Component\\Routing\\Matcher\\RequestMatcherInterface' => $vendorDir . '/symfony/routing/Matcher/RequestMatcherInterface.php', @@ -3198,6 +3203,9 @@ 'Symfony\\Component\\Translation\\Catalogue\\MergeOperation' => $vendorDir . '/symfony/translation/Catalogue/MergeOperation.php', 'Symfony\\Component\\Translation\\Catalogue\\OperationInterface' => $vendorDir . '/symfony/translation/Catalogue/OperationInterface.php', 'Symfony\\Component\\Translation\\Catalogue\\TargetOperation' => $vendorDir . '/symfony/translation/Catalogue/TargetOperation.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPullCommand' => $vendorDir . '/symfony/translation/Command/TranslationPullCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPushCommand' => $vendorDir . '/symfony/translation/Command/TranslationPushCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationTrait' => $vendorDir . '/symfony/translation/Command/TranslationTrait.php', 'Symfony\\Component\\Translation\\Command\\XliffLintCommand' => $vendorDir . '/symfony/translation/Command/XliffLintCommand.php', 'Symfony\\Component\\Translation\\DataCollectorTranslator' => $vendorDir . '/symfony/translation/DataCollectorTranslator.php', 'Symfony\\Component\\Translation\\DataCollector\\TranslationDataCollector' => $vendorDir . '/symfony/translation/DataCollector/TranslationDataCollector.php', @@ -3218,23 +3226,26 @@ 'Symfony\\Component\\Translation\\Dumper\\XliffFileDumper' => $vendorDir . '/symfony/translation/Dumper/XliffFileDumper.php', 'Symfony\\Component\\Translation\\Dumper\\YamlFileDumper' => $vendorDir . '/symfony/translation/Dumper/YamlFileDumper.php', 'Symfony\\Component\\Translation\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/translation/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\IncompleteDsnException' => $vendorDir . '/symfony/translation/Exception/IncompleteDsnException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/translation/Exception/InvalidArgumentException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidResourceException' => $vendorDir . '/symfony/translation/Exception/InvalidResourceException.php', 'Symfony\\Component\\Translation\\Exception\\LogicException' => $vendorDir . '/symfony/translation/Exception/LogicException.php', + 'Symfony\\Component\\Translation\\Exception\\MissingRequiredOptionException' => $vendorDir . '/symfony/translation/Exception/MissingRequiredOptionException.php', 'Symfony\\Component\\Translation\\Exception\\NotFoundResourceException' => $vendorDir . '/symfony/translation/Exception/NotFoundResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderException' => $vendorDir . '/symfony/translation/Exception/ProviderException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderExceptionInterface' => $vendorDir . '/symfony/translation/Exception/ProviderExceptionInterface.php', 'Symfony\\Component\\Translation\\Exception\\RuntimeException' => $vendorDir . '/symfony/translation/Exception/RuntimeException.php', + 'Symfony\\Component\\Translation\\Exception\\UnsupportedSchemeException' => $vendorDir . '/symfony/translation/Exception/UnsupportedSchemeException.php', 'Symfony\\Component\\Translation\\Extractor\\AbstractFileExtractor' => $vendorDir . '/symfony/translation/Extractor/AbstractFileExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ChainExtractor' => $vendorDir . '/symfony/translation/Extractor/ChainExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ExtractorInterface' => $vendorDir . '/symfony/translation/Extractor/ExtractorInterface.php', 'Symfony\\Component\\Translation\\Extractor\\PhpExtractor' => $vendorDir . '/symfony/translation/Extractor/PhpExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\PhpStringTokenParser' => $vendorDir . '/symfony/translation/Extractor/PhpStringTokenParser.php', - 'Symfony\\Component\\Translation\\Formatter\\ChoiceMessageFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatter' => $vendorDir . '/symfony/translation/Formatter/IntlFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/IntlFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatter' => $vendorDir . '/symfony/translation/Formatter/MessageFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/MessageFormatterInterface.php', 'Symfony\\Component\\Translation\\IdentityTranslator' => $vendorDir . '/symfony/translation/IdentityTranslator.php', - 'Symfony\\Component\\Translation\\Interval' => $vendorDir . '/symfony/translation/Interval.php', 'Symfony\\Component\\Translation\\Loader\\ArrayLoader' => $vendorDir . '/symfony/translation/Loader/ArrayLoader.php', 'Symfony\\Component\\Translation\\Loader\\CsvFileLoader' => $vendorDir . '/symfony/translation/Loader/CsvFileLoader.php', 'Symfony\\Component\\Translation\\Loader\\FileLoader' => $vendorDir . '/symfony/translation/Loader/FileLoader.php', @@ -3252,14 +3263,23 @@ 'Symfony\\Component\\Translation\\LoggingTranslator' => $vendorDir . '/symfony/translation/LoggingTranslator.php', 'Symfony\\Component\\Translation\\MessageCatalogue' => $vendorDir . '/symfony/translation/MessageCatalogue.php', 'Symfony\\Component\\Translation\\MessageCatalogueInterface' => $vendorDir . '/symfony/translation/MessageCatalogueInterface.php', - 'Symfony\\Component\\Translation\\MessageSelector' => $vendorDir . '/symfony/translation/MessageSelector.php', 'Symfony\\Component\\Translation\\MetadataAwareInterface' => $vendorDir . '/symfony/translation/MetadataAwareInterface.php', - 'Symfony\\Component\\Translation\\PluralizationRules' => $vendorDir . '/symfony/translation/PluralizationRules.php', + 'Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory' => $vendorDir . '/symfony/translation/Provider/AbstractProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\Dsn' => $vendorDir . '/symfony/translation/Provider/Dsn.php', + 'Symfony\\Component\\Translation\\Provider\\FilteringProvider' => $vendorDir . '/symfony/translation/Provider/FilteringProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProvider' => $vendorDir . '/symfony/translation/Provider/NullProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProviderFactory' => $vendorDir . '/symfony/translation/Provider/NullProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface' => $vendorDir . '/symfony/translation/Provider/ProviderFactoryInterface.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderInterface' => $vendorDir . '/symfony/translation/Provider/ProviderInterface.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollection' => $vendorDir . '/symfony/translation/Provider/TranslationProviderCollection.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollectionFactory' => $vendorDir . '/symfony/translation/Provider/TranslationProviderCollectionFactory.php', + 'Symfony\\Component\\Translation\\PseudoLocalizationTranslator' => $vendorDir . '/symfony/translation/PseudoLocalizationTranslator.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReader' => $vendorDir . '/symfony/translation/Reader/TranslationReader.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReaderInterface' => $vendorDir . '/symfony/translation/Reader/TranslationReaderInterface.php', + 'Symfony\\Component\\Translation\\TranslatableMessage' => $vendorDir . '/symfony/translation/TranslatableMessage.php', 'Symfony\\Component\\Translation\\Translator' => $vendorDir . '/symfony/translation/Translator.php', + 'Symfony\\Component\\Translation\\TranslatorBag' => $vendorDir . '/symfony/translation/TranslatorBag.php', 'Symfony\\Component\\Translation\\TranslatorBagInterface' => $vendorDir . '/symfony/translation/TranslatorBagInterface.php', - 'Symfony\\Component\\Translation\\TranslatorInterface' => $vendorDir . '/symfony/translation/TranslatorInterface.php', 'Symfony\\Component\\Translation\\Util\\ArrayConverter' => $vendorDir . '/symfony/translation/Util/ArrayConverter.php', 'Symfony\\Component\\Translation\\Util\\XliffUtils' => $vendorDir . '/symfony/translation/Util/XliffUtils.php', 'Symfony\\Component\\Translation\\Writer\\TranslationWriter' => $vendorDir . '/symfony/translation/Writer/TranslationWriter.php', diff --git a/composer/autoload_files.php b/composer/autoload_files.php index d1edfbfe9..260d6a6d7 100644 --- a/composer/autoload_files.php +++ b/composer/autoload_files.php @@ -121,4 +121,5 @@ '8a9dc1de0ca7e01f3e08231539562f61' => $vendorDir . '/aws/aws-sdk-php/src/functions.php', '538ca81a9a966a6716601ecf48f4eaef' => $vendorDir . '/opis/closure/functions.php', 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', ); diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 3ab99099b..968edfc64 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -122,6 +122,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 '8a9dc1de0ca7e01f3e08231539562f61' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/functions.php', '538ca81a9a966a6716601ecf48f4eaef' => __DIR__ . '/..' . '/opis/closure/functions.php', 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', ); public static $prefixLengthsPsr4 = array ( @@ -3790,40 +3791,45 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Process\\Pipes\\WindowsPipes' => __DIR__ . '/..' . '/symfony/process/Pipes/WindowsPipes.php', 'Symfony\\Component\\Process\\Process' => __DIR__ . '/..' . '/symfony/process/Process.php', '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\\CompiledRoute' => __DIR__ . '/..' . '/symfony/routing/CompiledRoute.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', 'Symfony\\Component\\Routing\\Exception\\InvalidParameterException' => __DIR__ . '/..' . '/symfony/routing/Exception/InvalidParameterException.php', 'Symfony\\Component\\Routing\\Exception\\MethodNotAllowedException' => __DIR__ . '/..' . '/symfony/routing/Exception/MethodNotAllowedException.php', 'Symfony\\Component\\Routing\\Exception\\MissingMandatoryParametersException' => __DIR__ . '/..' . '/symfony/routing/Exception/MissingMandatoryParametersException.php', 'Symfony\\Component\\Routing\\Exception\\NoConfigurationException' => __DIR__ . '/..' . '/symfony/routing/Exception/NoConfigurationException.php', 'Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException' => __DIR__ . '/..' . '/symfony/routing/Exception/ResourceNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RouteCircularReferenceException' => __DIR__ . '/..' . '/symfony/routing/Exception/RouteCircularReferenceException.php', 'Symfony\\Component\\Routing\\Exception\\RouteNotFoundException' => __DIR__ . '/..' . '/symfony/routing/Exception/RouteNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/routing/Exception/RuntimeException.php', 'Symfony\\Component\\Routing\\Generator\\CompiledUrlGenerator' => __DIR__ . '/..' . '/symfony/routing/Generator/CompiledUrlGenerator.php', 'Symfony\\Component\\Routing\\Generator\\ConfigurableRequirementsInterface' => __DIR__ . '/..' . '/symfony/routing/Generator/ConfigurableRequirementsInterface.php', 'Symfony\\Component\\Routing\\Generator\\Dumper\\CompiledUrlGeneratorDumper' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php', 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumper' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/GeneratorDumper.php', 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumperInterface' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php', - 'Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/PhpGeneratorDumper.php', 'Symfony\\Component\\Routing\\Generator\\UrlGenerator' => __DIR__ . '/..' . '/symfony/routing/Generator/UrlGenerator.php', 'Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface' => __DIR__ . '/..' . '/symfony/routing/Generator/UrlGeneratorInterface.php', '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\\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', 'Symfony\\Component\\Routing\\Loader\\Configurator\\ImportConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/ImportConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\RouteConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/RouteConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/RoutingConfigurator.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\AddTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/AddTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\HostTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/HostTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\LocalizedRouteTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\PrefixTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php', 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\RouteTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/RouteTrait.php', 'Symfony\\Component\\Routing\\Loader\\ContainerLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ContainerLoader.php', - 'Symfony\\Component\\Routing\\Loader\\DependencyInjection\\ServiceRouterLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/DependencyInjection/ServiceRouterLoader.php', 'Symfony\\Component\\Routing\\Loader\\DirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/DirectoryLoader.php', '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\\ObjectRouteLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ObjectRouteLoader.php', 'Symfony\\Component\\Routing\\Loader\\PhpFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/PhpFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\XmlFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/XmlFileLoader.php', 'Symfony\\Component\\Routing\\Loader\\YamlFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/YamlFileLoader.php', @@ -3832,8 +3838,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Routing\\Matcher\\Dumper\\CompiledUrlMatcherTrait' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php', 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumper' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/MatcherDumper.php', 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumperInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php', - 'Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php', 'Symfony\\Component\\Routing\\Matcher\\Dumper\\StaticPrefixCollection' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php', + 'Symfony\\Component\\Routing\\Matcher\\ExpressionLanguageProvider' => __DIR__ . '/..' . '/symfony/routing/Matcher/ExpressionLanguageProvider.php', 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcher' => __DIR__ . '/..' . '/symfony/routing/Matcher/RedirectableUrlMatcher.php', 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php', 'Symfony\\Component\\Routing\\Matcher\\RequestMatcherInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/RequestMatcherInterface.php', @@ -3867,6 +3873,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Translation\\Catalogue\\MergeOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/MergeOperation.php', 'Symfony\\Component\\Translation\\Catalogue\\OperationInterface' => __DIR__ . '/..' . '/symfony/translation/Catalogue/OperationInterface.php', 'Symfony\\Component\\Translation\\Catalogue\\TargetOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/TargetOperation.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPullCommand' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationPullCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPushCommand' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationPushCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationTrait' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationTrait.php', 'Symfony\\Component\\Translation\\Command\\XliffLintCommand' => __DIR__ . '/..' . '/symfony/translation/Command/XliffLintCommand.php', 'Symfony\\Component\\Translation\\DataCollectorTranslator' => __DIR__ . '/..' . '/symfony/translation/DataCollectorTranslator.php', 'Symfony\\Component\\Translation\\DataCollector\\TranslationDataCollector' => __DIR__ . '/..' . '/symfony/translation/DataCollector/TranslationDataCollector.php', @@ -3887,23 +3896,26 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Translation\\Dumper\\XliffFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/XliffFileDumper.php', 'Symfony\\Component\\Translation\\Dumper\\YamlFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/YamlFileDumper.php', 'Symfony\\Component\\Translation\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/translation/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\IncompleteDsnException' => __DIR__ . '/..' . '/symfony/translation/Exception/IncompleteDsnException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/translation/Exception/InvalidArgumentException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidResourceException' => __DIR__ . '/..' . '/symfony/translation/Exception/InvalidResourceException.php', 'Symfony\\Component\\Translation\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/translation/Exception/LogicException.php', + 'Symfony\\Component\\Translation\\Exception\\MissingRequiredOptionException' => __DIR__ . '/..' . '/symfony/translation/Exception/MissingRequiredOptionException.php', 'Symfony\\Component\\Translation\\Exception\\NotFoundResourceException' => __DIR__ . '/..' . '/symfony/translation/Exception/NotFoundResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderException' => __DIR__ . '/..' . '/symfony/translation/Exception/ProviderException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderExceptionInterface' => __DIR__ . '/..' . '/symfony/translation/Exception/ProviderExceptionInterface.php', 'Symfony\\Component\\Translation\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/translation/Exception/RuntimeException.php', + 'Symfony\\Component\\Translation\\Exception\\UnsupportedSchemeException' => __DIR__ . '/..' . '/symfony/translation/Exception/UnsupportedSchemeException.php', 'Symfony\\Component\\Translation\\Extractor\\AbstractFileExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/AbstractFileExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ChainExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/ChainExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ExtractorInterface' => __DIR__ . '/..' . '/symfony/translation/Extractor/ExtractorInterface.php', 'Symfony\\Component\\Translation\\Extractor\\PhpExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\PhpStringTokenParser' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpStringTokenParser.php', - 'Symfony\\Component\\Translation\\Formatter\\ChoiceMessageFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatter' => __DIR__ . '/..' . '/symfony/translation/Formatter/IntlFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/IntlFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatter' => __DIR__ . '/..' . '/symfony/translation/Formatter/MessageFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/MessageFormatterInterface.php', 'Symfony\\Component\\Translation\\IdentityTranslator' => __DIR__ . '/..' . '/symfony/translation/IdentityTranslator.php', - 'Symfony\\Component\\Translation\\Interval' => __DIR__ . '/..' . '/symfony/translation/Interval.php', 'Symfony\\Component\\Translation\\Loader\\ArrayLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/ArrayLoader.php', 'Symfony\\Component\\Translation\\Loader\\CsvFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/CsvFileLoader.php', 'Symfony\\Component\\Translation\\Loader\\FileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/FileLoader.php', @@ -3921,14 +3933,23 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Translation\\LoggingTranslator' => __DIR__ . '/..' . '/symfony/translation/LoggingTranslator.php', 'Symfony\\Component\\Translation\\MessageCatalogue' => __DIR__ . '/..' . '/symfony/translation/MessageCatalogue.php', 'Symfony\\Component\\Translation\\MessageCatalogueInterface' => __DIR__ . '/..' . '/symfony/translation/MessageCatalogueInterface.php', - 'Symfony\\Component\\Translation\\MessageSelector' => __DIR__ . '/..' . '/symfony/translation/MessageSelector.php', 'Symfony\\Component\\Translation\\MetadataAwareInterface' => __DIR__ . '/..' . '/symfony/translation/MetadataAwareInterface.php', - 'Symfony\\Component\\Translation\\PluralizationRules' => __DIR__ . '/..' . '/symfony/translation/PluralizationRules.php', + 'Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/AbstractProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\Dsn' => __DIR__ . '/..' . '/symfony/translation/Provider/Dsn.php', + 'Symfony\\Component\\Translation\\Provider\\FilteringProvider' => __DIR__ . '/..' . '/symfony/translation/Provider/FilteringProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProvider' => __DIR__ . '/..' . '/symfony/translation/Provider/NullProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProviderFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/NullProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface' => __DIR__ . '/..' . '/symfony/translation/Provider/ProviderFactoryInterface.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderInterface' => __DIR__ . '/..' . '/symfony/translation/Provider/ProviderInterface.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollection' => __DIR__ . '/..' . '/symfony/translation/Provider/TranslationProviderCollection.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollectionFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/TranslationProviderCollectionFactory.php', + 'Symfony\\Component\\Translation\\PseudoLocalizationTranslator' => __DIR__ . '/..' . '/symfony/translation/PseudoLocalizationTranslator.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReader' => __DIR__ . '/..' . '/symfony/translation/Reader/TranslationReader.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReaderInterface' => __DIR__ . '/..' . '/symfony/translation/Reader/TranslationReaderInterface.php', + 'Symfony\\Component\\Translation\\TranslatableMessage' => __DIR__ . '/..' . '/symfony/translation/TranslatableMessage.php', 'Symfony\\Component\\Translation\\Translator' => __DIR__ . '/..' . '/symfony/translation/Translator.php', + 'Symfony\\Component\\Translation\\TranslatorBag' => __DIR__ . '/..' . '/symfony/translation/TranslatorBag.php', 'Symfony\\Component\\Translation\\TranslatorBagInterface' => __DIR__ . '/..' . '/symfony/translation/TranslatorBagInterface.php', - 'Symfony\\Component\\Translation\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/translation/TranslatorInterface.php', 'Symfony\\Component\\Translation\\Util\\ArrayConverter' => __DIR__ . '/..' . '/symfony/translation/Util/ArrayConverter.php', 'Symfony\\Component\\Translation\\Util\\XliffUtils' => __DIR__ . '/..' . '/symfony/translation/Util/XliffUtils.php', 'Symfony\\Component\\Translation\\Writer\\TranslationWriter' => __DIR__ . '/..' . '/symfony/translation/Writer/TranslationWriter.php', diff --git a/composer/installed.json b/composer/installed.json index d9bf3ca59..478d34bca 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -5115,17 +5115,17 @@ }, { "name": "symfony/http-foundation", - "version": "v5.4.10", - "version_normalized": "5.4.10.0", + "version": "v5.4.25", + "version_normalized": "5.4.25.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e" + "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e7793b7906f72a8cc51054fbca9dcff7a8af1c1e", - "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f66be2706075c5f6325d2fe2b743a57fb5d23f6b", + "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b", "shasum": "" }, "require": { @@ -5137,13 +5137,16 @@ "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/mime": "^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" }, "suggest": { "symfony/mime": "To use the file extension guesser" }, - "time": "2022-06-19T13:13:40+00:00", + "time": "2023-06-22T08:06:06+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5171,7 +5174,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.10" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.25" }, "funding": [ { @@ -6098,45 +6101,46 @@ }, { "name": "symfony/routing", - "version": "v4.4.30", - "version_normalized": "4.4.30.0", + "version": "v5.4.25", + "version_normalized": "5.4.25.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e" + "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/9ddf033927ad9f30ba2bfd167a7b342cafa13e8e", - "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e", + "url": "https://api.github.com/repos/symfony/routing/zipball/56bfc1394f7011303eb2e22724f9b422d3f14649", + "reference": "56bfc1394f7011303eb2e22724f9b422d3f14649", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-php80": "^1.16" }, "conflict": { - "symfony/config": "<4.2", - "symfony/dependency-injection": "<3.4", - "symfony/yaml": "<3.4" + "doctrine/annotations": "<1.12", + "symfony/config": "<5.3", + "symfony/dependency-injection": "<4.4", + "symfony/yaml": "<4.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4", + "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0" + "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": { - "doctrine/annotations": "For using the annotation loader", "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" }, - "time": "2021-08-04T21:41:01+00:00", + "time": "2023-06-05T14:18:47+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6170,7 +6174,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v4.4.30" + "source": "https://github.com/symfony/routing/tree/v5.4.25" }, "funding": [ { @@ -6363,54 +6367,62 @@ }, { "name": "symfony/translation", - "version": "v4.4.41", - "version_normalized": "4.4.41.0", + "version": "v5.4.24", + "version_normalized": "5.4.24.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331" + "reference": "de237e59c5833422342be67402d487fbf50334ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", + "url": "https://api.github.com/repos/symfony/translation/zipball/de237e59c5833422342be67402d487fbf50334ff", + "reference": "de237e59c5833422342be67402d487fbf50334ff", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^1.1.6|^2" + "symfony/translation-contracts": "^2.3" }, "conflict": { - "symfony/config": "<3.4", - "symfony/dependency-injection": "<3.4", - "symfony/http-kernel": "<4.4", - "symfony/yaml": "<3.4" + "symfony/config": "<4.4", + "symfony/console": "<5.3", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" }, "provide": { - "symfony/translation-implementation": "1.0|2.0" + "symfony/translation-implementation": "2.3" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/finder": "~2.8|~3.0|~4.0|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/intl": "^3.4|^4.0|^5.0", - "symfony/service-contracts": "^1.1.2|^2", - "symfony/yaml": "^3.4|^4.0|^5.0" + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/http-client-contracts": "^1.1|^2.0|^3.0", + "symfony/http-kernel": "^5.0|^6.0", + "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/service-contracts": "^1.1.2|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" }, "suggest": { "psr/log-implementation": "To use logging capability in translator", "symfony/config": "", "symfony/yaml": "" }, - "time": "2022-04-21T07:22:34+00:00", + "time": "2023-05-19T12:34:17+00:00", "type": "library", "installation-source": "dist", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, @@ -6435,7 +6447,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v4.4.41" + "source": "https://github.com/symfony/translation/tree/v5.4.24" }, "funding": [ { diff --git a/composer/installed.php b/composer/installed.php index 4afcc6fd8..88e446d18 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'nextcloud/3rdparty', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '350e2a466ff849c59386e817c849f9e441fc10ab', + 'reference' => '176fea9ed19bf24d4620cb0b2ca1274201ad045c', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -310,7 +310,7 @@ 'nextcloud/3rdparty' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '350e2a466ff849c59386e817c849f9e441fc10ab', + 'reference' => '176fea9ed19bf24d4620cb0b2ca1274201ad045c', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -749,9 +749,9 @@ ), ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v5.4.10', - 'version' => '5.4.10.0', - 'reference' => 'e7793b7906f72a8cc51054fbca9dcff7a8af1c1e', + 'pretty_version' => 'v5.4.25', + 'version' => '5.4.25.0', + 'reference' => 'f66be2706075c5f6325d2fe2b743a57fb5d23f6b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), @@ -857,9 +857,9 @@ 'dev_requirement' => false, ), 'symfony/routing' => array( - 'pretty_version' => 'v4.4.30', - 'version' => '4.4.30.0', - 'reference' => '9ddf033927ad9f30ba2bfd167a7b342cafa13e8e', + 'pretty_version' => 'v5.4.25', + 'version' => '5.4.25.0', + 'reference' => '56bfc1394f7011303eb2e22724f9b422d3f14649', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/routing', 'aliases' => array(), @@ -884,9 +884,9 @@ 'dev_requirement' => false, ), 'symfony/translation' => array( - 'pretty_version' => 'v4.4.41', - 'version' => '4.4.41.0', - 'reference' => 'dcb67eae126e74507e0b4f0b9ac6ef35b37c3331', + 'pretty_version' => 'v5.4.24', + 'version' => '5.4.24.0', + 'reference' => 'de237e59c5833422342be67402d487fbf50334ff', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation', 'aliases' => array(), @@ -904,7 +904,7 @@ 'symfony/translation-implementation' => array( 'dev_requirement' => false, 'provided' => array( - 0 => '1.0|2.0', + 0 => '2.3', ), ), 'thecodingmachine/safe' => array( diff --git a/symfony/http-foundation/BinaryFileResponse.php b/symfony/http-foundation/BinaryFileResponse.php index 4769cab08..d3caa36aa 100644 --- a/symfony/http-foundation/BinaryFileResponse.php +++ b/symfony/http-foundation/BinaryFileResponse.php @@ -34,6 +34,7 @@ class BinaryFileResponse extends Response protected $offset = 0; protected $maxlen = -1; protected $deleteFileAfterSend = false; + protected $chunkSize = 16 * 1024; /** * @param \SplFileInfo|string $file The file to stream @@ -125,6 +126,22 @@ public function getFile() return $this->file; } + /** + * Sets the response stream chunk size. + * + * @return $this + */ + public function setChunkSize(int $chunkSize): self + { + 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.'); + } + + $this->chunkSize = $chunkSize; + + return $this; + } + /** * Automatically sets the Last-Modified header according the file modification date. * @@ -189,15 +206,19 @@ public function setContentDisposition(string $disposition, string $filename = '' */ public function prepare(Request $request) { - if (!$this->headers->has('Content-Type')) { - $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); + if ($this->isInformational() || $this->isEmpty()) { + parent::prepare($request); + + $this->maxlen = 0; + + return $this; } - if ('HTTP/1.0' !== $request->server->get('SERVER_PROTOCOL')) { - $this->setProtocolVersion('1.1'); + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); } - $this->ensureIEOverSSLCompatibility($request); + parent::prepare($request); $this->offset = 0; $this->maxlen = -1; @@ -205,6 +226,7 @@ public function prepare(Request $request) if (false === $fileSize = $this->file->getSize()) { return $this; } + $this->headers->remove('Transfer-Encoding'); $this->headers->set('Content-Length', $fileSize); if (!$this->headers->has('Accept-Ranges')) { @@ -245,7 +267,7 @@ public function prepare(Request $request) $range = $request->headers->get('Range'); if (str_starts_with($range, 'bytes=')) { - [$start, $end] = explode('-', substr($range, 6), 2) + [0]; + [$start, $end] = explode('-', substr($range, 6), 2) + [1 => 0]; $end = ('' === $end) ? $fileSize - 1 : (int) $end; @@ -274,6 +296,10 @@ public function prepare(Request $request) } } + if ($request->isMethod('HEAD')) { + $this->maxlen = 0; + } + return $this; } @@ -295,24 +321,49 @@ private function hasValidIfRangeHeader(?string $header): bool */ public function sendContent() { - if (!$this->isSuccessful()) { - return parent::sendContent(); - } + try { + if (!$this->isSuccessful()) { + return parent::sendContent(); + } - if (0 === $this->maxlen) { - return $this; - } + if (0 === $this->maxlen) { + return $this; + } - $out = fopen('php://output', 'w'); - $file = fopen($this->file->getPathname(), 'r'); + $out = fopen('php://output', 'w'); + $file = fopen($this->file->getPathname(), 'r'); - stream_copy_to_stream($file, $out, $this->maxlen, $this->offset); + ignore_user_abort(true); - fclose($out); - fclose($file); + if (0 !== $this->offset) { + fseek($file, $this->offset); + } - if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) { - unlink($this->file->getPathname()); + $length = $this->maxlen; + while ($length && !feof($file)) { + $read = $length > $this->chunkSize || 0 > $length ? $this->chunkSize : $length; + + if (false === $data = fread($file, $read)) { + break; + } + while ('' !== $data) { + $read = fwrite($out, $data); + if (false === $read || connection_aborted()) { + break 2; + } + if (0 < $length) { + $length -= $read; + } + $data = substr($data, $read); + } + } + + fclose($out); + fclose($file); + } finally { + if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) { + unlink($this->file->getPathname()); + } } return $this; diff --git a/symfony/http-foundation/Cookie.php b/symfony/http-foundation/Cookie.php index b4b26c015..91024535b 100644 --- a/symfony/http-foundation/Cookie.php +++ b/symfony/http-foundation/Cookie.php @@ -80,7 +80,7 @@ public static function create(string $name, string $value = null, $expire = 0, ? * @param string $name The name of the cookie * @param string|null $value The value of the cookie * @param int|string|\DateTimeInterface $expire The time the cookie expires - * @param string $path The path on the server in which the cookie will be available on + * @param string|null $path The path on the server in which the cookie will be available on * @param string|null $domain The domain that the cookie is available to * @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 diff --git a/symfony/http-foundation/Exception/SessionNotFoundException.php b/symfony/http-foundation/Exception/SessionNotFoundException.php index 9c719aa04..94b0cb69a 100644 --- a/symfony/http-foundation/Exception/SessionNotFoundException.php +++ b/symfony/http-foundation/Exception/SessionNotFoundException.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpFoundation\Exception; /** - * Raised when a session does not exists. This happens in the following cases: + * Raised when a session does not exist. This happens in the following cases: * - the session is not enabled * - attempt to read a session outside a request context (ie. cli script). * diff --git a/symfony/http-foundation/File/UploadedFile.php b/symfony/http-foundation/File/UploadedFile.php index cf50a02ca..fcc629913 100644 --- a/symfony/http-foundation/File/UploadedFile.php +++ b/symfony/http-foundation/File/UploadedFile.php @@ -223,8 +223,8 @@ public function move(string $directory, string $name = null) */ public static function getMaxFilesize() { - $sizePostMax = self::parseFilesize(ini_get('post_max_size')); - $sizeUploadMax = self::parseFilesize(ini_get('upload_max_filesize')); + $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); + $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); } @@ -253,8 +253,11 @@ private static function parseFilesize(string $size) switch (substr($size, -1)) { case 't': $max *= 1024; + // no break case 'g': $max *= 1024; + // no break case 'm': $max *= 1024; + // no break case 'k': $max *= 1024; } diff --git a/symfony/http-foundation/HeaderUtils.php b/symfony/http-foundation/HeaderUtils.php index 1d56be080..46b1e6aed 100644 --- a/symfony/http-foundation/HeaderUtils.php +++ b/symfony/http-foundation/HeaderUtils.php @@ -138,7 +138,7 @@ public static function quote(string $s): string * Decodes a quoted string. * * If passed an unquoted string that matches the "token" construct (as - * defined in the HTTP specification), it is passed through verbatimly. + * defined in the HTTP specification), it is passed through verbatim. */ public static function unquote(string $s): string { diff --git a/symfony/http-foundation/InputBag.php b/symfony/http-foundation/InputBag.php index b36001d8b..a9d3cd82a 100644 --- a/symfony/http-foundation/InputBag.php +++ b/symfony/http-foundation/InputBag.php @@ -29,14 +29,14 @@ final class InputBag extends ParameterBag */ public function get(string $key, $default = null) { - if (null !== $default && !is_scalar($default) && !(\is_object($default) && method_exists($default, '__toString'))) { + 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__); } $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-string 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) && !(\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__); } return $this === $value ? $default : $value; @@ -76,7 +76,7 @@ public function add(array $inputs = []) */ public function set(string $key, $value) { - if (null !== $value && !is_scalar($value) && !\is_array($value) && !method_exists($value, '__toString')) { + 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__); } diff --git a/symfony/http-foundation/IpUtils.php b/symfony/http-foundation/IpUtils.php index 24111f1ec..49d9a9d74 100644 --- a/symfony/http-foundation/IpUtils.php +++ b/symfony/http-foundation/IpUtils.php @@ -73,7 +73,7 @@ public static function checkIp4(?string $requestIp, string $ip) return false; } - $cacheKey = $requestIp.'-'.$ip; + $cacheKey = $requestIp.'-'.$ip.'-v4'; if (isset(self::$checkedIps[$cacheKey])) { return self::$checkedIps[$cacheKey]; } @@ -86,7 +86,7 @@ public static function checkIp4(?string $requestIp, string $ip) [$address, $netmask] = explode('/', $ip, 2); if ('0' === $netmask) { - return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + return self::$checkedIps[$cacheKey] = false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); } if ($netmask < 0 || $netmask > 32) { @@ -126,7 +126,7 @@ public static function checkIp6(?string $requestIp, string $ip) return false; } - $cacheKey = $requestIp.'-'.$ip; + $cacheKey = $requestIp.'-'.$ip.'-v6'; if (isset(self::$checkedIps[$cacheKey])) { return self::$checkedIps[$cacheKey]; } @@ -135,9 +135,18 @@ public static function checkIp6(?string $requestIp, string $ip) throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); } + // 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; + } + if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); + if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + if ('0' === $netmask) { return (bool) unpack('n*', @inet_pton($address)); } @@ -146,6 +155,10 @@ public static function checkIp6(?string $requestIp, string $ip) return self::$checkedIps[$cacheKey] = false; } } else { + if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + $address = $ip; $netmask = 128; } diff --git a/symfony/http-foundation/LICENSE b/symfony/http-foundation/LICENSE index 88bf75bb4..0138f8f07 100644 --- a/symfony/http-foundation/LICENSE +++ b/symfony/http-foundation/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2022 Fabien Potencier +Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/http-foundation/ParameterBag.php b/symfony/http-foundation/ParameterBag.php index 7d051abe7..e1f89d69e 100644 --- a/symfony/http-foundation/ParameterBag.php +++ b/symfony/http-foundation/ParameterBag.php @@ -39,7 +39,7 @@ public function __construct(array $parameters = []) * * @return array */ - public function all(/*string $key = null*/) + public function all(/* string $key = null */) { $key = \func_num_args() > 0 ? func_get_arg(0) : null; diff --git a/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php index c91d614fe..a6dd993b7 100644 --- a/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php +++ b/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php @@ -35,9 +35,7 @@ public function consume(Request $request): RateLimit foreach ($limiters as $limiter) { $rateLimit = $limiter->consume(1); - if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) { - $minimalRateLimit = $rateLimit; - } + $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; } return $minimalRateLimit; @@ -54,4 +52,20 @@ public function reset(Request $request): void * @return LimiterInterface[] a set of limiters using keys extracted from the request */ abstract protected function getLimiters(Request $request): array; + + private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit + { + if ($first->isAccepted() !== $second->isAccepted()) { + return $first->isAccepted() ? $second : $first; + } + + $firstRemainingTokens = $first->getRemainingTokens(); + $secondRemainingTokens = $second->getRemainingTokens(); + + if ($firstRemainingTokens === $secondRemainingTokens) { + return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; + } + + return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; + } } diff --git a/symfony/http-foundation/Request.php b/symfony/http-foundation/Request.php index d112b1f18..28cebad16 100644 --- a/symfony/http-foundation/Request.php +++ b/symfony/http-foundation/Request.php @@ -191,7 +191,7 @@ class Request protected $session; /** - * @var string + * @var string|null */ protected $locale; @@ -439,12 +439,12 @@ public static function setFactory(?callable $callable) /** * Clones a request and overrides some of its parameters. * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters + * @param array|null $query The GET parameters + * @param array|null $request The POST parameters + * @param array|null $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array|null $cookies The COOKIE parameters + * @param array|null $files The FILES parameters + * @param array|null $server The SERVER parameters * * @return static */ @@ -522,10 +522,10 @@ public function __toString() $cookies = []; foreach ($this->cookies as $k => $v) { - $cookies[] = $k.'='.$v; + $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v"; } - if (!empty($cookies)) { + if ($cookies) { $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n"; } @@ -562,7 +562,7 @@ public function overrideGlobals() $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE]; - $requestOrder = ini_get('request_order') ?: ini_get('variables_order'); + $requestOrder = \ini_get('request_order') ?: \ini_get('variables_order'); $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp'; $_REQUEST = [[]]; @@ -1452,7 +1452,7 @@ public function setLocale(string $locale) */ public function getLocale() { - return null === $this->locale ? $this->defaultLocale : $this->locale; + return $this->locale ?? $this->defaultLocale; } /** @@ -1573,9 +1573,9 @@ public function getContent(bool $asResource = false) /** * Gets the request body decoded as array, typically from a JSON payload. * - * @throws JsonException When the body cannot be decoded to an array - * * @return array + * + * @throws JsonException When the body cannot be decoded to an array */ public function toArray() { @@ -1689,7 +1689,8 @@ public function getLanguages() $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all(); $this->languages = []; - foreach ($languages as $lang => $acceptHeaderItem) { + foreach ($languages as $acceptHeaderItem) { + $lang = $acceptHeaderItem->getValue(); if (str_contains($lang, '-')) { $codes = explode('-', $lang); if ('i' === $codes[0]) { @@ -1727,7 +1728,7 @@ public function getCharsets() return $this->charsets; } - return $this->charsets = 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())); } /** @@ -1741,7 +1742,7 @@ public function getEncodings() return $this->encodings; } - return $this->encodings = 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())); } /** @@ -1755,7 +1756,7 @@ public function getAcceptableContentTypes() return $this->acceptableContentTypes; } - return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all()); + return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); } /** diff --git a/symfony/http-foundation/Response.php b/symfony/http-foundation/Response.php index def7f8e70..59e974d62 100644 --- a/symfony/http-foundation/Response.php +++ b/symfony/http-foundation/Response.php @@ -72,7 +72,7 @@ class Response public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585 public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585 public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585 - public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; + public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; // RFC7725 public const HTTP_INTERNAL_SERVER_ERROR = 500; public const HTTP_NOT_IMPLEMENTED = 501; public const HTTP_BAD_GATEWAY = 502; @@ -399,6 +399,7 @@ public function send() litespeed_finish_request(); } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { static::closeOutputBuffers(0, true); + flush(); } return $this; @@ -773,8 +774,10 @@ public function getMaxAge(): ?int return (int) $this->headers->getCacheControlDirective('max-age'); } - if (null !== $this->getExpires()) { - return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U'); + if (null !== $expires = $this->getExpires()) { + $maxAge = (int) $expires->format('U') - (int) $this->getDate()->format('U'); + + return max($maxAge, 0); } return null; @@ -818,7 +821,7 @@ public function setSharedMaxAge(int $value): object * * It returns null when no freshness information is present in the response. * - * When the responses TTL is <= 0, the response may not be served from cache without first + * When the response's TTL is 0, the response may not be served from cache without first * revalidating with the origin. * * @final @@ -827,7 +830,7 @@ public function getTtl(): ?int { $maxAge = $this->getMaxAge(); - return null !== $maxAge ? $maxAge - $this->getAge() : null; + return null !== $maxAge ? max($maxAge - $this->getAge(), 0) : null; } /** diff --git a/symfony/http-foundation/Session/SessionInterface.php b/symfony/http-foundation/Session/SessionInterface.php index b2f09fd0d..e67338337 100644 --- a/symfony/http-foundation/Session/SessionInterface.php +++ b/symfony/http-foundation/Session/SessionInterface.php @@ -59,10 +59,10 @@ public function setName(string $name); * Clears all session attributes and flashes and regenerates the * session and deletes the old session from persistence. * - * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value - * 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. + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * 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 */ @@ -72,11 +72,11 @@ public function invalidate(int $lifetime = null); * Migrates the current session to a new session id while maintaining all * session attributes. * - * @param bool $destroy Whether to delete the old session or leave it to garbage collection - * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value - * 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. + * @param bool $destroy Whether to delete the old session or leave it to garbage collection + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * 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 */ diff --git a/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php index bc7b944fc..35d7b4b81 100644 --- a/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php @@ -35,8 +35,8 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess public function open($savePath, $sessionName) { $this->sessionName = $sessionName; - if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) { - header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire'))); + if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { + header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); } return true; @@ -126,7 +126,7 @@ public function write($sessionId, $data) #[\ReturnTypeWillChange] public function destroy($sessionId) { - if (!headers_sent() && filter_var(ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) { + if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) { if (!$this->sessionName) { throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); } @@ -141,7 +141,7 @@ public function destroy($sessionId) */ 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)); + 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']); diff --git a/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php index a5a78eb95..e0ec4d2d9 100644 --- a/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -77,7 +77,7 @@ protected function doRead(string $sessionId) #[\ReturnTypeWillChange] public function updateTimestamp($sessionId, $data) { - $this->memcached->touch($this->prefix.$sessionId, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime'))); + $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); return true; } @@ -87,7 +87,20 @@ public function updateTimestamp($sessionId, $data) */ protected function doWrite(string $sessionId, string $data) { - return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime'))); + return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); + } + + private function getCompatibleTtl(): int + { + $ttl = (int) ($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. + if ($ttl > 60 * 60 * 24 * 30) { + $ttl += time(); + } + + return $ttl; } /** diff --git a/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php index 8384e79dd..ef8f71942 100644 --- a/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -121,7 +121,7 @@ public function gc($maxlifetime) */ protected function doWrite(string $sessionId, string $data) { - $expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000); + $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); $fields = [ $this->options['time_field'] => new UTCDateTime(), @@ -144,7 +144,7 @@ protected function doWrite(string $sessionId, string $data) #[\ReturnTypeWillChange] public function updateTimestamp($sessionId, $data) { - $expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000); + $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); $this->getCollection()->updateOne( [$this->options['id_field'] => $sessionId], diff --git a/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php index effc9db54..52a103879 100644 --- a/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php @@ -19,9 +19,9 @@ class NativeFileSessionHandler extends \SessionHandler { /** - * @param string $savePath Path of directory to save session files - * Default null will leave setting as defined by PHP. - * '/path', 'N;/path', or 'N;octal-mode;/path + * @param string|null $savePath Path of directory to save session files + * Default null will leave setting as defined by PHP. + * '/path', 'N;/path', or 'N;octal-mode;/path * * @see https://php.net/session.configuration#ini.session.save-path for further details. * @@ -31,7 +31,7 @@ class NativeFileSessionHandler extends \SessionHandler public function __construct(string $savePath = null) { if (null === $savePath) { - $savePath = ini_get('session.save_path'); + $savePath = \ini_get('session.save_path'); } $baseDir = $savePath; @@ -49,7 +49,11 @@ public function __construct(string $savePath = null) throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $baseDir)); } - ini_set('session.save_path', $savePath); - ini_set('session.save_handler', 'files'); + if ($savePath !== \ini_get('session.save_path')) { + ini_set('session.save_path', $savePath); + } + if ('files' !== \ini_get('session.save_handler')) { + ini_set('session.save_handler', 'files'); + } } } diff --git a/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php index 067bfcb36..cad7e0a72 100644 --- a/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php @@ -344,7 +344,7 @@ protected function doDestroy(string $sessionId) */ protected function doWrite(string $sessionId, string $data) { - $maxlifetime = (int) ini_get('session.gc_maxlifetime'); + $maxlifetime = (int) \ini_get('session.gc_maxlifetime'); try { // We use a single MERGE SQL query when supported by the database. @@ -391,14 +391,14 @@ protected function doWrite(string $sessionId, string $data) #[\ReturnTypeWillChange] public function updateTimestamp($sessionId, $data) { - $expiry = time() + (int) ini_get('session.gc_maxlifetime'); + $expiry = time() + (int) \ini_get('session.gc_maxlifetime'); try { $updateStmt = $this->pdo->prepare( "UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id" ); - $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT); + $updateStmt->bindValue(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindValue(':expiry', $expiry, \PDO::PARAM_INT); $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); $updateStmt->execute(); } catch (\PDOException $e) { @@ -530,8 +530,8 @@ private function buildDsnFromUrl(string $dsnOrUrl): string return $dsn; } } - // If "unix_socket" is not in the query, we continue with the same process as pgsql - // no break + // If "unix_socket" is not in the query, we continue with the same process as pgsql + // no break case 'pgsql': $dsn ?? $dsn = 'pgsql:'; @@ -687,7 +687,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_BOOLEAN) && 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 @@ -935,7 +935,7 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife protected function getConnection() { if (null === $this->pdo) { - $this->connect($this->dsn ?: ini_get('session.save_path')); + $this->connect($this->dsn ?: \ini_get('session.save_path')); } return $this->pdo; diff --git a/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php index 9573b3116..31954e677 100644 --- a/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php @@ -79,7 +79,7 @@ protected function doRead(string $sessionId): string */ protected function doWrite(string $sessionId, string $data): bool { - $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')), $data); + $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')), $data); return $result && !$result instanceof ErrorInterface; } @@ -132,6 +132,6 @@ public function gc($maxlifetime) #[\ReturnTypeWillChange] public function updateTimestamp($sessionId, $data) { - return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime'))); + return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime'))); } } diff --git a/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php index f3f7b201d..39dc30c6f 100644 --- a/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -11,7 +11,10 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; +use Doctrine\DBAL\Configuration; use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Tools\DsnParser; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Traits\RedisClusterProxy; use Symfony\Component\Cache\Traits\RedisProxy; @@ -71,7 +74,15 @@ public static function createHandler($connection): AbstractSessionHandler if (!class_exists(DriverManager::class)) { throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); } - $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection(); + $connection[3] = '-'; + $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; + $config = new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $connection = DriverManager::getConnection($params, $config); + $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); // no break; case str_starts_with($connection, 'mssql://'): diff --git a/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php index 0461e997e..f7c385f64 100644 --- a/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php +++ b/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php @@ -30,6 +30,16 @@ public function __construct(\SessionHandlerInterface $handler) $this->handler = $handler; } + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + * + * @internal + */ + public function isWrapper(): bool + { + return $this->handler instanceof \SessionHandler; + } + /** * @return bool */ diff --git a/symfony/http-foundation/Session/Storage/MetadataBag.php b/symfony/http-foundation/Session/Storage/MetadataBag.php index 1bfce5520..52d332094 100644 --- a/symfony/http-foundation/Session/Storage/MetadataBag.php +++ b/symfony/http-foundation/Session/Storage/MetadataBag.php @@ -95,10 +95,10 @@ public function getLifetime() /** * Stamps a new session's metadata. * - * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value - * 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. + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * 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. */ public function stampNew(int $lifetime = null) { @@ -162,6 +162,6 @@ private function stampCreated(int $lifetime = null): void { $timeStamp = time(); $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; - $this->meta[self::LIFETIME] = $lifetime ?? (int) ini_get('session.cookie_lifetime'); + $this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime'); } } diff --git a/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/symfony/http-foundation/Session/Storage/NativeSessionStorage.php index 4440dccc6..242478c42 100644 --- a/symfony/http-foundation/Session/Storage/NativeSessionStorage.php +++ b/symfony/http-foundation/Session/Storage/NativeSessionStorage.php @@ -141,12 +141,42 @@ 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_BOOLEAN) && 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)); } $sessionId = $_COOKIE[session_name()] ?? null; - if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,}$/', $sessionId)) { + /* + * Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`. + * + * ---------- Part 1 + * + * The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6. + * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character. + * Allowed values are integers such as: + * - 4 for range `a-f0-9` + * - 5 for range `a-v0-9` + * - 6 for range `a-zA-Z0-9,-` + * + * ---------- Part 2 + * + * The part `{22,250}` is related to the PHP ini directive `session.sid_length`. + * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length. + * Allowed values are integers between 22 and 256, but we use 250 for the max. + * + * Where does the 250 come from? + * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255. + * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250. + * + * ---------- Conclusion + * + * The parts 1 and 2 prevent the warning below: + * `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.` + * + * The part 2 prevents the warning below: + * `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).` + */ + if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) { // the session ID in the header is invalid, create a new one session_id(session_create_id()); } @@ -214,7 +244,7 @@ public function regenerate(bool $destroy = false, int $lifetime = null) return false; } - if (null !== $lifetime && $lifetime != ini_get('session.cookie_lifetime')) { + if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) { $this->save(); ini_set('session.cookie_lifetime', $lifetime); $this->start(); @@ -249,7 +279,7 @@ public function save() unset($_SESSION[$key]); } } - if ([$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { + if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { unset($_SESSION[$key]); } @@ -425,9 +455,10 @@ public function setOptions(array $options) */ public function setSaveHandler($saveHandler = null) { - if (!$saveHandler instanceof AbstractProxy && - !$saveHandler instanceof \SessionHandlerInterface && - null !== $saveHandler) { + if (!$saveHandler instanceof AbstractProxy + && !$saveHandler instanceof \SessionHandlerInterface + && null !== $saveHandler + ) { throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); } diff --git a/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php b/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php index 9b0cdeb7f..0defa4a7a 100644 --- a/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + /** * @author Drak */ @@ -22,7 +24,7 @@ public function __construct(\SessionHandlerInterface $handler) { $this->handler = $handler; $this->wrapper = $handler instanceof \SessionHandler; - $this->saveHandlerName = $this->wrapper ? ini_get('session.save_handler') : 'user'; + $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; } /** diff --git a/symfony/http-foundation/Session/Storage/SessionStorageInterface.php b/symfony/http-foundation/Session/Storage/SessionStorageInterface.php index b7f66e7c7..705374552 100644 --- a/symfony/http-foundation/Session/Storage/SessionStorageInterface.php +++ b/symfony/http-foundation/Session/Storage/SessionStorageInterface.php @@ -80,11 +80,11 @@ public function setName(string $name); * Otherwise session data could get lost again for concurrent requests with the * new ID. One result could be that you get logged out after just logging in. * - * @param bool $destroy Destroy session when regenerating? - * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value - * 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. + * @param bool $destroy Destroy session when regenerating? + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * 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 * diff --git a/symfony/http-foundation/StreamedResponse.php b/symfony/http-foundation/StreamedResponse.php index 676cd6687..0599bd1e4 100644 --- a/symfony/http-foundation/StreamedResponse.php +++ b/symfony/http-foundation/StreamedResponse.php @@ -114,9 +114,9 @@ public function sendContent() /** * {@inheritdoc} * - * @throws \LogicException when the content is not null - * * @return $this + * + * @throws \LogicException when the content is not null */ public function setContent(?string $content) { diff --git a/symfony/http-foundation/UrlHelper.php b/symfony/http-foundation/UrlHelper.php index c15f101cd..90659947d 100644 --- a/symfony/http-foundation/UrlHelper.php +++ b/symfony/http-foundation/UrlHelper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; /** * A helper service for manipulating URLs within and outside the request scope. @@ -23,8 +24,15 @@ final class UrlHelper private $requestStack; private $requestContext; - public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) + /** + * @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; } @@ -73,28 +81,36 @@ public function getRelativePath(string $path): string private function getAbsoluteUrlFromContext(string $path): string { - if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { return $path; } - $scheme = $this->requestContext->getScheme(); + $scheme = $context->getScheme(); $port = ''; - if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { - $port = ':'.$this->requestContext->getHttpPort(); - } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { - $port = ':'.$this->requestContext->getHttpsPort(); + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); } if ('#' === $path[0]) { - $queryString = $this->requestContext->getQueryString(); - $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { - $path = $this->requestContext->getPathInfo().$path; + $path = $context->getPathInfo().$path; } if ('/' !== $path[0]) { - $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; } return $scheme.'://'.$host.$port.$path; diff --git a/symfony/routing/Alias.php b/symfony/routing/Alias.php new file mode 100644 index 000000000..f3e1d5a85 --- /dev/null +++ b/symfony/routing/Alias.php @@ -0,0 +1,96 @@ + + * + * 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\Routing\Exception\InvalidArgumentException; + +class Alias +{ + private $id; + private $deprecation = []; + + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * @return static + */ + public function withId(string $id): self + { + $new = clone $this; + + $new->id = $id; + + return $new; + } + + /** + * Returns the target name of this alias. + * + * @return string The target name + */ + public function getId(): string + { + return $this->id; + } + + /** + * Whether this alias is deprecated, that means it should not be referenced anymore. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The deprecation message to use + * + * @return $this + * + * @throws InvalidArgumentException when the message template is invalid + */ + public function setDeprecated(string $package, string $version, string $message): self + { + if ('' !== $message) { + if (preg_match('#[\r\n]|\*/#', $message)) { + throw new InvalidArgumentException('Invalid characters found in deprecation template.'); + } + + if (!str_contains($message, '%alias_id%')) { + throw new InvalidArgumentException('The deprecation template must contain the "%alias_id%" placeholder.'); + } + } + + $this->deprecation = [ + 'package' => $package, + 'version' => $version, + 'message' => $message ?: 'The "%alias_id%" route alias is deprecated. You should stop using it, as it will be removed in the future.', + ]; + + return $this; + } + + public function isDeprecated(): bool + { + return (bool) $this->deprecation; + } + + /** + * @param string $name Route name relying on this alias + */ + public function getDeprecation(string $name): array + { + return [ + 'package' => $this->deprecation['package'], + 'version' => $this->deprecation['version'], + 'message' => str_replace('%alias_id%', $name, $this->deprecation['message']), + ]; + } +} diff --git a/symfony/routing/Annotation/Route.php b/symfony/routing/Annotation/Route.php index dc1cee319..81563df20 100644 --- a/symfony/routing/Annotation/Route.php +++ b/symfony/routing/Annotation/Route.php @@ -15,10 +15,13 @@ * 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; @@ -31,14 +34,79 @@ class Route private $methods = []; private $schemes = []; private $condition; + private $priority; + private $env; /** - * @param array $data An array of key/value parameters + * @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(array $data) - { + 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)); } @@ -68,6 +136,11 @@ public function __construct(array $data) 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)) { @@ -77,7 +150,7 @@ public function __construct(array $data) } } - public function setPath($path) + public function setPath(string $path) { $this->path = $path; } @@ -97,7 +170,7 @@ public function getLocalizedPaths(): array return $this->localizedPaths; } - public function setHost($pattern) + public function setHost(string $pattern) { $this->host = $pattern; } @@ -107,7 +180,7 @@ public function getHost() return $this->host; } - public function setName($name) + public function setName(string $name) { $this->name = $name; } @@ -117,7 +190,7 @@ public function getName() return $this->name; } - public function setRequirements($requirements) + public function setRequirements(array $requirements) { $this->requirements = $requirements; } @@ -127,7 +200,7 @@ public function getRequirements() return $this->requirements; } - public function setOptions($options) + public function setOptions(array $options) { $this->options = $options; } @@ -137,7 +210,7 @@ public function getOptions() return $this->options; } - public function setDefaults($defaults) + public function setDefaults(array $defaults) { $this->defaults = $defaults; } @@ -167,7 +240,7 @@ public function getMethods() return $this->methods; } - public function setCondition($condition) + public function setCondition(?string $condition) { $this->condition = $condition; } @@ -176,4 +249,24 @@ 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; + } } diff --git a/symfony/routing/CompiledRoute.php b/symfony/routing/CompiledRoute.php index b6f31b2eb..1449cdb92 100644 --- a/symfony/routing/CompiledRoute.php +++ b/symfony/routing/CompiledRoute.php @@ -64,12 +64,9 @@ public function __serialize(): array } /** - * @return string - * - * @internal since Symfony 4.3 - * @final since Symfony 4.3 + * @internal */ - public function serialize() + final public function serialize(): string { return serialize($this->__serialize()); } @@ -87,10 +84,9 @@ public function __unserialize(array $data): void } /** - * @internal since Symfony 4.3 - * @final since Symfony 4.3 + * @internal */ - public function unserialize($serialized) + final public function unserialize($serialized) { $this->__unserialize(unserialize($serialized, ['allowed_classes' => false])); } @@ -98,7 +94,7 @@ public function unserialize($serialized) /** * Returns the static prefix. * - * @return string The static prefix + * @return string */ public function getStaticPrefix() { @@ -108,7 +104,7 @@ public function getStaticPrefix() /** * Returns the regex. * - * @return string The regex + * @return string */ public function getRegex() { @@ -118,7 +114,7 @@ public function getRegex() /** * Returns the host regex. * - * @return string|null The host regex or null + * @return string|null */ public function getHostRegex() { @@ -128,7 +124,7 @@ public function getHostRegex() /** * Returns the tokens. * - * @return array The tokens + * @return array */ public function getTokens() { @@ -138,7 +134,7 @@ public function getTokens() /** * Returns the host tokens. * - * @return array The tokens + * @return array */ public function getHostTokens() { @@ -148,7 +144,7 @@ public function getHostTokens() /** * Returns the variables. * - * @return array The variables + * @return array */ public function getVariables() { @@ -158,7 +154,7 @@ public function getVariables() /** * Returns the path variables. * - * @return array The variables + * @return array */ public function getPathVariables() { @@ -168,7 +164,7 @@ public function getPathVariables() /** * Returns the host variables. * - * @return array The variables + * @return array */ public function getHostVariables() { diff --git a/symfony/routing/DependencyInjection/RoutingResolverPass.php b/symfony/routing/DependencyInjection/RoutingResolverPass.php index 7068825fe..0e9b9c893 100644 --- a/symfony/routing/DependencyInjection/RoutingResolverPass.php +++ b/symfony/routing/DependencyInjection/RoutingResolverPass.php @@ -30,6 +30,10 @@ class RoutingResolverPass implements CompilerPassInterface 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; } diff --git a/symfony/routing/Exception/InvalidArgumentException.php b/symfony/routing/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..950b9b15c --- /dev/null +++ b/symfony/routing/Exception/InvalidArgumentException.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\Routing\Exception; + +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/symfony/routing/Exception/MethodNotAllowedException.php b/symfony/routing/Exception/MethodNotAllowedException.php index 8a4371403..27cf2125e 100644 --- a/symfony/routing/Exception/MethodNotAllowedException.php +++ b/symfony/routing/Exception/MethodNotAllowedException.php @@ -27,6 +27,12 @@ class MethodNotAllowedException extends \RuntimeException implements ExceptionIn */ 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); diff --git a/symfony/routing/Exception/RouteCircularReferenceException.php b/symfony/routing/Exception/RouteCircularReferenceException.php new file mode 100644 index 000000000..841e35989 --- /dev/null +++ b/symfony/routing/Exception/RouteCircularReferenceException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +class RouteCircularReferenceException extends RuntimeException +{ + public function __construct(string $routeId, array $path) + { + parent::__construct(sprintf('Circular reference detected for route "%s", path: "%s".', $routeId, implode(' -> ', $path))); + } +} diff --git a/symfony/routing/Exception/RuntimeException.php b/symfony/routing/Exception/RuntimeException.php new file mode 100644 index 000000000..48da62ec8 --- /dev/null +++ b/symfony/routing/Exception/RuntimeException.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\Routing\Exception; + +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/symfony/routing/Generator/CompiledUrlGenerator.php b/symfony/routing/Generator/CompiledUrlGenerator.php index 05a01f287..8cbbf8f70 100644 --- a/symfony/routing/Generator/CompiledUrlGenerator.php +++ b/symfony/routing/Generator/CompiledUrlGenerator.php @@ -31,7 +31,7 @@ public function __construct(array $compiledRoutes, RequestContext $context, Logg $this->defaultLocale = $defaultLocale; } - public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH) { $locale = $parameters['_locale'] ?? $this->context->getParameter('_locale') @@ -50,7 +50,11 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); } - [$variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes] = $this->compiledRoutes[$name]; + [$variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes, $deprecations] = $this->compiledRoutes[$name] + [6 => []]; + + foreach ($deprecations as $deprecation) { + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { if (!\in_array('_locale', $variables, true)) { diff --git a/symfony/routing/Generator/ConfigurableRequirementsInterface.php b/symfony/routing/Generator/ConfigurableRequirementsInterface.php index 2e5dc5325..568f7f775 100644 --- a/symfony/routing/Generator/ConfigurableRequirementsInterface.php +++ b/symfony/routing/Generator/ConfigurableRequirementsInterface.php @@ -40,10 +40,8 @@ interface ConfigurableRequirementsInterface /** * Enables or disables the exception on incorrect parameters. * Passing null will deactivate the requirements check completely. - * - * @param bool|null $enabled */ - public function setStrictRequirements($enabled); + public function setStrictRequirements(?bool $enabled); /** * Returns whether to throw an exception on incorrect parameters. diff --git a/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php b/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php index e90a40a26..9c6740b61 100644 --- a/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php +++ b/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Generator\Dumper; +use Symfony\Component\Routing\Exception\RouteCircularReferenceException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; /** @@ -35,12 +37,57 @@ public function getCompiledRoutes(): array $compiledRoute->getTokens(), $compiledRoute->getHostTokens(), $route->getSchemes(), + [], ]; } return $compiledRoutes; } + public function getCompiledAliases(): array + { + $routes = $this->getRoutes(); + $compiledAliases = []; + foreach ($routes->getAliases() as $name => $alias) { + $deprecations = $alias->isDeprecated() ? [$alias->getDeprecation($name)] : []; + $currentId = $alias->getId(); + $visited = []; + while (null !== $alias = $routes->getAlias($currentId) ?? null) { + if (false !== $searchKey = array_search($currentId, $visited)) { + $visited[] = $currentId; + + throw new RouteCircularReferenceException($currentId, \array_slice($visited, $searchKey)); + } + + if ($alias->isDeprecated()) { + $deprecations[] = $deprecation = $alias->getDeprecation($currentId); + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } + + $visited[] = $currentId; + $currentId = $alias->getId(); + } + + if (null === $target = $routes->get($currentId)) { + throw new RouteNotFoundException(sprintf('Target route "%s" for alias "%s" does not exist.', $currentId, $name)); + } + + $compiledTarget = $target->compile(); + + $compiledAliases[$name] = [ + $compiledTarget->getVariables(), + $target->getDefaults(), + $target->getRequirements(), + $compiledTarget->getTokens(), + $compiledTarget->getHostTokens(), + $target->getSchemes(), + $deprecations, + ]; + } + + return $compiledAliases; + } + /** * {@inheritdoc} */ @@ -68,6 +115,10 @@ private function generateDeclaredRoutes(): string $routes .= sprintf("\n '%s' => %s,", $name, CompiledUrlMatcherDumper::export($properties)); } + foreach ($this->getCompiledAliases() as $alias => $properties) { + $routes .= sprintf("\n '%s' => %s,", $alias, CompiledUrlMatcherDumper::export($properties)); + } + return $routes; } } diff --git a/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php b/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php index 26daefc63..d4a248a5b 100644 --- a/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php +++ b/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php @@ -24,14 +24,14 @@ 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 Executable code + * @return string */ public function dump(array $options = []); /** * Gets the routes to dump. * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection */ public function getRoutes(); } diff --git a/symfony/routing/Generator/Dumper/PhpGeneratorDumper.php b/symfony/routing/Generator/Dumper/PhpGeneratorDumper.php deleted file mode 100644 index 23081f917..000000000 --- a/symfony/routing/Generator/Dumper/PhpGeneratorDumper.php +++ /dev/null @@ -1,143 +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\Generator\Dumper; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "CompiledUrlGeneratorDumper" instead.', PhpGeneratorDumper::class), \E_USER_DEPRECATED); - -use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; - -/** - * PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes. - * - * @author Fabien Potencier - * @author Tobias Schultze - * - * @deprecated since Symfony 4.3, use CompiledUrlGeneratorDumper instead. - */ -class PhpGeneratorDumper extends GeneratorDumper -{ - /** - * Dumps a set of routes to a PHP class. - * - * Available options: - * - * * class: The class name - * * base_class: The base class name - * - * @param array $options An array of options - * - * @return string A PHP class representing the generator class - */ - public function dump(array $options = []) - { - $options = array_merge([ - 'class' => 'ProjectUrlGenerator', - 'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator', - ], $options); - - return <<context = \$context; - \$this->logger = \$logger; - \$this->defaultLocale = \$defaultLocale; - if (null === self::\$declaredRoutes) { - self::\$declaredRoutes = {$this->generateDeclaredRoutes()}; - } - } - -{$this->generateGenerateMethod()} -} - -EOF; - } - - /** - * Generates PHP code representing an array of defined routes - * together with the routes properties (e.g. requirements). - */ - private function generateDeclaredRoutes(): string - { - $routes = "[\n"; - foreach ($this->getRoutes()->all() as $name => $route) { - $compiledRoute = $route->compile(); - - $properties = []; - $properties[] = $compiledRoute->getVariables(); - $properties[] = $route->getDefaults(); - $properties[] = $route->getRequirements(); - $properties[] = $compiledRoute->getTokens(); - $properties[] = $compiledRoute->getHostTokens(); - $properties[] = $route->getSchemes(); - - $routes .= sprintf(" '%s' => %s,\n", $name, CompiledUrlMatcherDumper::export($properties)); - } - $routes .= ' ]'; - - return $routes; - } - - /** - * Generates PHP code representing the `generate` method that implements the UrlGeneratorInterface. - */ - private function generateGenerateMethod(): string - { - return <<<'EOF' - public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) - { - $locale = $parameters['_locale'] - ?? $this->context->getParameter('_locale') - ?: $this->defaultLocale; - - if (null !== $locale && null !== $name) { - do { - if ((self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) { - $name .= '.'.$locale; - break; - } - } while (false !== $locale = strstr($locale, '_', true)); - } - - if (!isset(self::$declaredRoutes[$name])) { - throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); - } - - list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = self::$declaredRoutes[$name]; - - if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { - if (!\in_array('_locale', $variables, true)) { - unset($parameters['_locale']); - } elseif (!isset($parameters['_locale'])) { - $parameters['_locale'] = $defaults['_locale']; - } - } - - return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes); - } -EOF; - } -} diff --git a/symfony/routing/Generator/UrlGenerator.php b/symfony/routing/Generator/UrlGenerator.php index 4ed870479..d27b00004 100644 --- a/symfony/routing/Generator/UrlGenerator.php +++ b/symfony/routing/Generator/UrlGenerator.php @@ -66,6 +66,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt // some webservers don't allow the slash in encoded form in the path for security reasons anyway // see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss '%2F' => '/', + '%252F' => '%2F', // the following chars are general delimiters in the URI specification but have only special meaning in the authority component // so they can safely be used in the path in unencoded form '%40' => '@', @@ -108,9 +109,9 @@ public function getContext() /** * {@inheritdoc} */ - public function setStrictRequirements($enabled) + public function setStrictRequirements(?bool $enabled) { - $this->strictRequirements = null === $enabled ? null : (bool) $enabled; + $this->strictRequirements = $enabled; } /** @@ -124,7 +125,7 @@ public function isStrictRequirements() /** * {@inheritdoc} */ - public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH) { $route = null; $locale = $parameters['_locale'] @@ -161,13 +162,13 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT } /** + * @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 - * - * @return string */ - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = []) + protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []) { $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); @@ -294,6 +295,17 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa return $a == $b ? 0 : 1; }); + array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { + if (\is_object($v)) { + if ($vars = get_object_vars($v)) { + array_walk_recursive($vars, $caster); + $v = $vars; + } elseif (method_exists($v, '__toString')) { + $v = (string) $v; + } + } + }); + // extract fragment $fragment = $defaults['_fragment'] ?? ''; @@ -331,9 +343,9 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa * @param string $basePath The base path * @param string $targetPath The target path * - * @return string The relative target path + * @return string */ - public static function getRelativePath($basePath, $targetPath) + public static function getRelativePath(string $basePath, string $targetPath) { if ($basePath === $targetPath) { return ''; diff --git a/symfony/routing/Generator/UrlGeneratorInterface.php b/symfony/routing/Generator/UrlGeneratorInterface.php index 2550619a1..c6d5005f9 100644 --- a/symfony/routing/Generator/UrlGeneratorInterface.php +++ b/symfony/routing/Generator/UrlGeneratorInterface.php @@ -71,16 +71,12 @@ interface UrlGeneratorInterface extends RequestContextAwareInterface * * The special parameter _fragment will be used as the document fragment suffixed to the final URL. * - * @param string $name The name of the route - * @param mixed[] $parameters An array of parameters - * @param int $referenceType The type of reference to be generated (one of the constants) - * - * @return string The generated 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($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH); + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH); } diff --git a/symfony/routing/LICENSE b/symfony/routing/LICENSE index 9ff2d0d63..0138f8f07 100644 --- a/symfony/routing/LICENSE +++ b/symfony/routing/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2021 Fabien Potencier +Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/routing/Loader/AnnotationClassLoader.php b/symfony/routing/Loader/AnnotationClassLoader.php index ca7f46b16..ad5af5c94 100644 --- a/symfony/routing/Loader/AnnotationClassLoader.php +++ b/symfony/routing/Loader/AnnotationClassLoader.php @@ -18,7 +18,6 @@ use Symfony\Component\Routing\Annotation\Route as RouteAnnotation; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\RouteCompiler; /** * AnnotationClassLoader loads routing information from a PHP class and its methods. @@ -52,34 +51,50 @@ * { * } * } + * + * 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 = 'Symfony\\Component\\Routing\\Annotation\\Route'; + protected $routeAnnotationClass = RouteAnnotation::class; /** * @var int */ protected $defaultRouteIndex = 0; - public function __construct(Reader $reader) + public function __construct(Reader $reader = null, string $env = null) { $this->reader = $reader; + $this->env = $env; } /** * Sets the annotation class to read route properties from. - * - * @param string $class A fully-qualified class name */ - public function setRouteAnnotationClass($class) + public function setRouteAnnotationClass(string $class) { $this->routeAnnotationClass = $class; } @@ -87,14 +102,13 @@ public function setRouteAnnotationClass($class) /** * Loads from annotations from a class. * - * @param string $class A class name - * @param string|null $type The resource type + * @param string $class A class name * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection * * @throws \InvalidArgumentException When route can't be parsed */ - public function load($class, $type = null) + public function load($class, string $type = null) { if (!class_exists($class)) { throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); @@ -110,21 +124,21 @@ public function load($class, $type = null) $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->reader->getMethodAnnotations($method) as $annot) { - if ($annot instanceof $this->routeAnnotationClass) { - $this->addRoute($collection, $annot, $globals, $class, $method); - } + 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->reader->getClassAnnotations($class) as $annot) { - if ($annot instanceof $this->routeAnnotationClass) { - $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); - } + foreach ($this->getAnnotations($class) as $annot) { + $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); } } @@ -132,11 +146,14 @@ public function load($class, $type = null) } /** - * @param RouteAnnotation $annot or an object that exposes a similar interface - * @param array $globals + * @param RouteAnnotation $annot or an object that exposes a similar interface */ - protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method) + 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); @@ -147,7 +164,7 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl foreach ($requirements as $placeholder => $requirement) { if (\is_int($placeholder)) { - @trigger_error(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()), \E_USER_DEPRECATED); + 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())); } } @@ -162,10 +179,8 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl $host = $globals['host']; } - $condition = $annot->getCondition(); - if (null === $condition) { - $condition = $globals['condition']; - } + $condition = $annot->getCondition() ?? $globals['condition']; + $priority = $annot->getPriority() ?? $globals['priority']; $path = $annot->getLocalizedPaths() ?: $annot->getPath(); $prefix = $globals['localized_paths'] ?: $globals['path']; @@ -212,11 +227,11 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl $this->configureRoute($route, $class, $method, $annot); if (0 !== $locale) { $route->setDefault('_locale', $locale); - $route->setRequirement('_locale', preg_quote($locale, RouteCompiler::REGEX_DELIMITER)); + $route->setRequirement('_locale', preg_quote($locale)); $route->setDefault('_canonical_route', $name); - $collection->add($name.'.'.$locale, $route); + $collection->add($name.'.'.$locale, $route, $priority); } else { - $collection->add($name, $route); + $collection->add($name, $route, $priority); } } } @@ -224,7 +239,7 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type); } @@ -264,7 +279,15 @@ protected function getGlobals(\ReflectionClass $class) { $globals = $this->resetGlobals(); - if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { + $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(); } @@ -303,9 +326,12 @@ protected function getGlobals(\ReflectionClass $class) $globals['condition'] = $annot->getCondition(); } + $globals['priority'] = $annot->getPriority() ?? 0; + $globals['env'] = $annot->getEnv(); + foreach ($globals['requirements'] as $placeholder => $requirement) { if (\is_int($placeholder)) { - @trigger_error(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()), \E_USER_DEPRECATED); + 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())); } } } @@ -313,7 +339,7 @@ protected function getGlobals(\ReflectionClass $class) return $globals; } - private function resetGlobals() + private function resetGlobals(): array { return [ 'path' => null, @@ -326,13 +352,43 @@ private function resetGlobals() 'host' => '', 'condition' => '', 'name' => '', + 'priority' => 0, + 'env' => null, ]; } - protected function createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition) + 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, $annot); + 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 df43321e4..ae825a39f 100644 --- a/symfony/routing/Loader/AnnotationDirectoryLoader.php +++ b/symfony/routing/Loader/AnnotationDirectoryLoader.php @@ -28,11 +28,11 @@ class AnnotationDirectoryLoader extends AnnotationFileLoader * @param string $path A directory path * @param string|null $type The resource type * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection * * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed */ - public function load($path, $type = null) + 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(); @@ -54,7 +54,7 @@ function (\SplFileInfo $current) { }); foreach ($files as $file) { - if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { + if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { continue; } @@ -74,7 +74,7 @@ function (\SplFileInfo $current) { /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { if ('annotation' === $type) { return true; diff --git a/symfony/routing/Loader/AnnotationFileLoader.php b/symfony/routing/Loader/AnnotationFileLoader.php index 6db5a22f0..27af66ee6 100644 --- a/symfony/routing/Loader/AnnotationFileLoader.php +++ b/symfony/routing/Loader/AnnotationFileLoader.php @@ -43,11 +43,11 @@ public function __construct(FileLocatorInterface $locator, AnnotationClassLoader * @param string $file A PHP file path * @param string|null $type The resource type * - * @return RouteCollection|null A RouteCollection instance + * @return RouteCollection|null * * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed */ - public function load($file, $type = null) + public function load($file, string $type = null) { $path = $this->locator->locate($file); @@ -70,7 +70,7 @@ public function load($file, $type = null) /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'annotation' === $type); } @@ -78,11 +78,9 @@ public function supports($resource, $type = null) /** * Returns the full class name for the first class in the file. * - * @param string $file A PHP file path - * - * @return string|false Full class name if found, false otherwise + * @return string|false */ - protected function findClass($file) + protected function findClass(string $file) { $class = false; $namespace = false; diff --git a/symfony/routing/Loader/ClosureLoader.php b/symfony/routing/Loader/ClosureLoader.php index 5df9f6ae8..42f950f50 100644 --- a/symfony/routing/Loader/ClosureLoader.php +++ b/symfony/routing/Loader/ClosureLoader.php @@ -29,17 +29,17 @@ class ClosureLoader extends Loader * @param \Closure $closure A Closure * @param string|null $type The resource type * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection */ - public function load($closure, $type = null) + public function load($closure, string $type = null) { - return $closure(); + return $closure($this->env); } /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return $resource instanceof \Closure && (!$type || 'closure' === $type); } diff --git a/symfony/routing/Loader/Configurator/AliasConfigurator.php b/symfony/routing/Loader/Configurator/AliasConfigurator.php new file mode 100644 index 000000000..4b2206e68 --- /dev/null +++ b/symfony/routing/Loader/Configurator/AliasConfigurator.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\Routing\Loader\Configurator; + +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\Routing\Alias; + +class AliasConfigurator +{ + private $alias; + + public function __construct(Alias $alias) + { + $this->alias = $alias; + } + + /** + * Whether this alias is deprecated, that means it should not be called anymore. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The deprecation message to use + * + * @return $this + * + * @throws InvalidArgumentException when the message template is invalid + */ + public function deprecate(string $package, string $version, string $message): self + { + $this->alias->setDeprecated($package, $version, $message); + + return $this; + } +} diff --git a/symfony/routing/Loader/Configurator/CollectionConfigurator.php b/symfony/routing/Loader/Configurator/CollectionConfigurator.php index 270e853e8..09274ccdc 100644 --- a/symfony/routing/Loader/Configurator/CollectionConfigurator.php +++ b/symfony/routing/Loader/Configurator/CollectionConfigurator.php @@ -20,11 +20,13 @@ class CollectionConfigurator { use Traits\AddTrait; + use Traits\HostTrait; use Traits\RouteTrait; private $parent; private $parentConfigurator; private $parentPrefixes; + private $host; public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null, array $parentPrefixes = null) { @@ -54,6 +56,9 @@ public function __destruct() if (null === $this->prefixes) { $this->collection->addPrefix($this->route->getPath()); } + if (null !== $this->host) { + $this->addHost($this->collection, $this->host); + } $this->parent->addCollection($this->collection); } @@ -99,6 +104,20 @@ final public function prefix($prefix): self return $this; } + /** + * Sets the host to use for all child routes. + * + * @param string|array $host the host, or the localized hosts + * + * @return $this + */ + final public function host($host): self + { + $this->host = $host; + + return $this; + } + 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 b4dfb88b3..32f3efe3a 100644 --- a/symfony/routing/Loader/Configurator/ImportConfigurator.php +++ b/symfony/routing/Loader/Configurator/ImportConfigurator.php @@ -11,15 +11,15 @@ namespace Symfony\Component\Routing\Loader\Configurator; -use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\RouteCompiler; /** * @author Nicolas Grekas */ class ImportConfigurator { + use Traits\HostTrait; + use Traits\PrefixTrait; use Traits\RouteTrait; private $parent; @@ -57,39 +57,7 @@ public function __destruct() */ final public function prefix($prefix, bool $trailingSlashOnRoot = true): self { - if (!\is_array($prefix)) { - $this->route->addPrefix($prefix); - if (!$trailingSlashOnRoot) { - $rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath(); - foreach ($this->route->all() as $route) { - if ($route->getPath() === $rootPath) { - $route->setPath(rtrim($rootPath, '/')); - } - } - } - } else { - foreach ($prefix as $locale => $localePrefix) { - $prefix[$locale] = trim(trim($localePrefix), '/'); - } - foreach ($this->route->all() as $name => $route) { - if (null === $locale = $route->getDefault('_locale')) { - $this->route->remove($name); - foreach ($prefix as $locale => $localePrefix) { - $localizedRoute = clone $route; - $localizedRoute->setDefault('_locale', $locale); - $localizedRoute->setRequirement('_locale', preg_quote($locale, RouteCompiler::REGEX_DELIMITER)); - $localizedRoute->setDefault('_canonical_route', $name); - $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $this->route->add($name.'.'.$locale, $localizedRoute); - } - } 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())); - $this->route->add($name, $route); - } - } - } + $this->addPrefix($this->route, $prefix, $trailingSlashOnRoot); return $this; } @@ -105,4 +73,18 @@ final public function namePrefix(string $namePrefix): self return $this; } + + /** + * Sets the host to use for all child routes. + * + * @param string|array $host the host, or the localized hosts + * + * @return $this + */ + final public function host($host): self + { + $this->addHost($this->route, $host); + + return $this; + } } diff --git a/symfony/routing/Loader/Configurator/RouteConfigurator.php b/symfony/routing/Loader/Configurator/RouteConfigurator.php index e700f8de7..bb6ce267a 100644 --- a/symfony/routing/Loader/Configurator/RouteConfigurator.php +++ b/symfony/routing/Loader/Configurator/RouteConfigurator.php @@ -19,11 +19,12 @@ class RouteConfigurator { use Traits\AddTrait; + use Traits\HostTrait; use Traits\RouteTrait; - private $parentConfigurator; + protected $parentConfigurator; - public function __construct(RouteCollection $collection, $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; @@ -31,4 +32,18 @@ public function __construct(RouteCollection $collection, $route, string $name = $this->parentConfigurator = $parentConfigurator; // for GC control $this->prefixes = $prefixes; } + + /** + * Sets the host to use for all child routes. + * + * @param string|array $host the host, or the localized hosts + * + * @return $this + */ + final public function host($host): self + { + $this->addHost($this->route, $host); + + return $this; + } } diff --git a/symfony/routing/Loader/Configurator/RoutingConfigurator.php b/symfony/routing/Loader/Configurator/RoutingConfigurator.php index 8ed06f307..4687bf681 100644 --- a/symfony/routing/Loader/Configurator/RoutingConfigurator.php +++ b/symfony/routing/Loader/Configurator/RoutingConfigurator.php @@ -24,13 +24,15 @@ class RoutingConfigurator private $loader; private $path; private $file; + private $env; - public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file) + public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, string $env = null) { $this->collection = $collection; $this->loader = $loader; $this->path = $path; $this->file = $file; + $this->env = $env; } /** @@ -57,4 +59,23 @@ final public function collection(string $name = ''): CollectionConfigurator { return new CollectionConfigurator($this->collection, $name); } + + /** + * Get the current environment to be able to write conditional configuration. + */ + final public function env(): ?string + { + return $this->env; + } + + /** + * @return static + */ + final public function withPath(string $path): self + { + $clone = clone $this; + $clone->path = $clone->file = $path; + + return $clone; + } } diff --git a/symfony/routing/Loader/Configurator/Traits/AddTrait.php b/symfony/routing/Loader/Configurator/Traits/AddTrait.php index 84899aa2e..92b1bd0ea 100644 --- a/symfony/routing/Loader/Configurator/Traits/AddTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/AddTrait.php @@ -11,68 +11,41 @@ namespace Symfony\Component\Routing\Loader\Configurator\Traits; +use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; -use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\RouteCompiler; +/** + * @author Nicolas Grekas + */ trait AddTrait { + use LocalizedRouteTrait; + /** * @var RouteCollection */ - private $collection; - - private $name = ''; - - private $prefixes; + protected $collection; + protected $name = ''; + protected $prefixes; /** * Adds a route. * * @param string|array $path the path, or the localized paths of the route */ - final public function add(string $name, $path): RouteConfigurator + public function add(string $name, $path): RouteConfigurator { - $paths = []; $parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null); + $route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes); - if (\is_array($path)) { - if (null === $this->prefixes) { - $paths = $path; - } elseif ($missing = array_diff_key($this->prefixes, $path)) { - throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing)))); - } else { - foreach ($path as $locale => $localePath) { - if (!isset($this->prefixes[$locale])) { - throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); - } - - $paths[$locale] = $this->prefixes[$locale].$localePath; - } - } - } elseif (null !== $this->prefixes) { - foreach ($this->prefixes as $locale => $prefix) { - $paths[$locale] = $prefix.$path; - } - } else { - $this->collection->add($this->name.$name, $route = $this->createRoute($path)); - - return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes); - } - - $routes = new RouteCollection(); - - foreach ($paths as $locale => $path) { - $routes->add($name.'.'.$locale, $route = $this->createRoute($path)); - $this->collection->add($this->name.$name.'.'.$locale, $route); - $route->setDefault('_locale', $locale); - $route->setRequirement('_locale', preg_quote($locale, RouteCompiler::REGEX_DELIMITER)); - $route->setDefault('_canonical_route', $this->name.$name); - } + return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes); + } - return new RouteConfigurator($this->collection, $routes, $this->name, $parentConfigurator, $this->prefixes); + public function alias(string $name, string $alias): AliasConfigurator + { + return new AliasConfigurator($this->collection->addAlias($name, $alias)); } /** @@ -80,13 +53,8 @@ final public function add(string $name, $path): RouteConfigurator * * @param string|array $path the path, or the localized paths of the route */ - final public function __invoke(string $name, $path): RouteConfigurator + public function __invoke(string $name, $path): RouteConfigurator { return $this->add($name, $path); } - - private function createRoute(string $path): Route - { - return new Route($path); - } } diff --git a/symfony/routing/Loader/Configurator/Traits/HostTrait.php b/symfony/routing/Loader/Configurator/Traits/HostTrait.php new file mode 100644 index 000000000..54ae6566a --- /dev/null +++ b/symfony/routing/Loader/Configurator/Traits/HostTrait.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\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\RouteCollection; + +/** + * @internal + */ +trait HostTrait +{ + final protected function addHost(RouteCollection $routes, $hosts) + { + if (!$hosts || !\is_array($hosts)) { + $routes->setHost($hosts ?: ''); + + return; + } + + foreach ($routes->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $routes->remove($name); + foreach ($hosts as $locale => $host) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setRequirement('_locale', preg_quote($locale)); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setHost($host); + $routes->add($name.'.'.$locale, $localizedRoute); + } + } elseif (!isset($hosts[$locale])) { + throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding host in its parent collection.', $name, $locale)); + } else { + $route->setHost($hosts[$locale]); + $route->setRequirement('_locale', preg_quote($locale)); + $routes->add($name, $route); + } + } + } +} diff --git a/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php b/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php new file mode 100644 index 000000000..4734a4eac --- /dev/null +++ b/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.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\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @internal + * + * @author Nicolas Grekas + * @author Jules Pietri + */ +trait LocalizedRouteTrait +{ + /** + * Creates one or many routes. + * + * @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 + { + $paths = []; + + $routes = new RouteCollection(); + + if (\is_array($path)) { + if (null === $prefixes) { + $paths = $path; + } elseif ($missing = array_diff_key($prefixes, $path)) { + throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing)))); + } else { + foreach ($path as $locale => $localePath) { + if (!isset($prefixes[$locale])) { + throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); + } + + $paths[$locale] = $prefixes[$locale].$localePath; + } + } + } elseif (null !== $prefixes) { + foreach ($prefixes as $locale => $prefix) { + $paths[$locale] = $prefix.$path; + } + } else { + $routes->add($namePrefix.$name, $route = $this->createRoute($path)); + $collection->add($namePrefix.$name, $route); + + return $routes; + } + + foreach ($paths as $locale => $path) { + $routes->add($name.'.'.$locale, $route = $this->createRoute($path)); + $collection->add($namePrefix.$name.'.'.$locale, $route); + $route->setDefault('_locale', $locale); + $route->setRequirement('_locale', preg_quote($locale)); + $route->setDefault('_canonical_route', $namePrefix.$name); + } + + return $routes; + } + + private function createRoute(string $path): Route + { + return new Route($path); + } +} diff --git a/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php b/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php new file mode 100644 index 000000000..27053bcaf --- /dev/null +++ b/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @internal + * + * @author Nicolas Grekas + */ +trait PrefixTrait +{ + final protected function addPrefix(RouteCollection $routes, $prefix, bool $trailingSlashOnRoot) + { + if (\is_array($prefix)) { + foreach ($prefix as $locale => $localePrefix) { + $prefix[$locale] = trim(trim($localePrefix), '/'); + } + foreach ($routes->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $routes->remove($name); + foreach ($prefix as $locale => $localePrefix) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setRequirement('_locale', preg_quote($locale)); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); + $routes->add($name.'.'.$locale, $localizedRoute); + } + } 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); + } + } + + return; + } + + $routes->addPrefix($prefix); + if (!$trailingSlashOnRoot) { + $rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath(); + foreach ($routes->all() as $route) { + if ($route->getPath() === $rootPath) { + $route->setPath(rtrim($rootPath, '/')); + } + } + } + } +} diff --git a/symfony/routing/Loader/Configurator/Traits/RouteTrait.php b/symfony/routing/Loader/Configurator/Traits/RouteTrait.php index 22df1af05..ac05d10e5 100644 --- a/symfony/routing/Loader/Configurator/Traits/RouteTrait.php +++ b/symfony/routing/Loader/Configurator/Traits/RouteTrait.php @@ -19,7 +19,7 @@ trait RouteTrait /** * @var RouteCollection|Route */ - private $route; + protected $route; /** * Adds defaults. @@ -160,4 +160,16 @@ final public function format(string $format): self return $this; } + + /** + * Adds the "_stateless" entry to defaults. + * + * @return $this + */ + final public function stateless(bool $stateless = true): self + { + $this->route->addDefaults(['_stateless' => $stateless]); + + return $this; + } } diff --git a/symfony/routing/Loader/ContainerLoader.php b/symfony/routing/Loader/ContainerLoader.php index f248011ba..d8730aec6 100644 --- a/symfony/routing/Loader/ContainerLoader.php +++ b/symfony/routing/Loader/ContainerLoader.php @@ -22,15 +22,16 @@ class ContainerLoader extends ObjectLoader { private $container; - public function __construct(ContainerInterface $container) + public function __construct(ContainerInterface $container, string $env = null) { $this->container = $container; + parent::__construct($env); } /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return 'service' === $type && \is_string($resource); } diff --git a/symfony/routing/Loader/DependencyInjection/ServiceRouterLoader.php b/symfony/routing/Loader/DependencyInjection/ServiceRouterLoader.php deleted file mode 100644 index ab2be1026..000000000 --- a/symfony/routing/Loader/DependencyInjection/ServiceRouterLoader.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\Component\Routing\Loader\DependencyInjection; - -use Psr\Container\ContainerInterface; -use Symfony\Component\Routing\Loader\ContainerLoader; -use Symfony\Component\Routing\Loader\ObjectRouteLoader; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ServiceRouterLoader::class, ContainerLoader::class), \E_USER_DEPRECATED); - -/** - * A route loader that executes a service to load the routes. - * - * @author Ryan Weaver - * - * @deprecated since Symfony 4.4, use Symfony\Component\Routing\Loader\ContainerLoader instead. - */ -class ServiceRouterLoader extends ObjectRouteLoader -{ - /** - * @var ContainerInterface - */ - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - protected function getServiceObject($id) - { - return $this->container->get($id); - } -} diff --git a/symfony/routing/Loader/DirectoryLoader.php b/symfony/routing/Loader/DirectoryLoader.php index 08e833e0a..c0f349177 100644 --- a/symfony/routing/Loader/DirectoryLoader.php +++ b/symfony/routing/Loader/DirectoryLoader.php @@ -20,7 +20,7 @@ class DirectoryLoader extends FileLoader /** * {@inheritdoc} */ - public function load($file, $type = null) + public function load($file, string $type = null) { $path = $this->locator->locate($file); @@ -49,7 +49,7 @@ public function load($file, $type = null) /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { // only when type is forced to directory, not to conflict with AnnotationLoader diff --git a/symfony/routing/Loader/GlobFileLoader.php b/symfony/routing/Loader/GlobFileLoader.php index 03ee341b9..780fb15dc 100644 --- a/symfony/routing/Loader/GlobFileLoader.php +++ b/symfony/routing/Loader/GlobFileLoader.php @@ -24,7 +24,7 @@ class GlobFileLoader extends FileLoader /** * {@inheritdoc} */ - public function load($resource, $type = null) + public function load($resource, string $type = null) { $collection = new RouteCollection(); @@ -40,7 +40,7 @@ public function load($resource, $type = null) /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return 'glob' === $type; } diff --git a/symfony/routing/Loader/ObjectLoader.php b/symfony/routing/Loader/ObjectLoader.php index 344a79b70..062453908 100644 --- a/symfony/routing/Loader/ObjectLoader.php +++ b/symfony/routing/Loader/ObjectLoader.php @@ -40,36 +40,31 @@ abstract protected function getObject(string $id); * * @return RouteCollection */ - public function load($resource, $type = null) + public function load($resource, string $type = null) { - if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) { + 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')); } - if (1 === substr_count($resource, ':')) { - $resource = str_replace(':', '::', $resource); - @trigger_error(sprintf('Referencing object route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), \E_USER_DEPRECATED); - } - $parts = explode('::', $resource); $method = $parts[1] ?? '__invoke'; $loaderObject = $this->getObject($parts[0]); if (!\is_object($loaderObject)) { - throw new \TypeError(sprintf('"%s:getObject()" must return an object: "%s" returned.', static::class, \gettype($loaderObject))); + throw new \TypeError(sprintf('"%s:getObject()" must return an object: "%s" returned.', static::class, get_debug_type($loaderObject))); } if (!\is_callable([$loaderObject, $method])) { - throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s".', $method, \get_class($loaderObject), $resource)); + throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s".', $method, get_debug_type($loaderObject), $resource)); } - $routeCollection = $loaderObject->$method($this); + $routeCollection = $loaderObject->$method($this, $this->env); if (!$routeCollection instanceof RouteCollection) { - $type = \is_object($routeCollection) ? \get_class($routeCollection) : \gettype($routeCollection); + $type = get_debug_type($routeCollection); - throw new \LogicException(sprintf('The "%s::%s()" method must return a RouteCollection: "%s" returned.', \get_class($loaderObject), $method, $type)); + throw new \LogicException(sprintf('The "%s::%s()" method must return a RouteCollection: "%s" returned.', get_debug_type($loaderObject), $method, $type)); } // make the object file tracked so that if it changes, the cache rebuilds diff --git a/symfony/routing/Loader/ObjectRouteLoader.php b/symfony/routing/Loader/ObjectRouteLoader.php deleted file mode 100644 index 44f28fb8a..000000000 --- a/symfony/routing/Loader/ObjectRouteLoader.php +++ /dev/null @@ -1,52 +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\Loader; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ObjectRouteLoader::class, ObjectLoader::class), \E_USER_DEPRECATED); - -/** - * A route loader that calls a method on an object to load the routes. - * - * @author Ryan Weaver - * - * @deprecated since Symfony 4.4, use ObjectLoader instead. - */ -abstract class ObjectRouteLoader extends ObjectLoader -{ - /** - * Returns the object that the method will be called on to load routes. - * - * For example, if your application uses a service container, - * the $id may be a service id. - * - * @param string $id - * - * @return object - */ - abstract protected function getServiceObject($id); - - /** - * {@inheritdoc} - */ - public function supports($resource, $type = null) - { - return 'service' === $type; - } - - /** - * {@inheritdoc} - */ - protected function getObject(string $id) - { - return $this->getServiceObject($id); - } -} diff --git a/symfony/routing/Loader/PhpFileLoader.php b/symfony/routing/Loader/PhpFileLoader.php index 054290bff..39ac81273 100644 --- a/symfony/routing/Loader/PhpFileLoader.php +++ b/symfony/routing/Loader/PhpFileLoader.php @@ -22,6 +22,8 @@ * The file must return a RouteCollection instance. * * @author Fabien Potencier + * @author Nicolas grekas + * @author Jules Pietri */ class PhpFileLoader extends FileLoader { @@ -31,9 +33,9 @@ class PhpFileLoader extends FileLoader * @param string $file A PHP file path * @param string|null $type The resource type * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection */ - public function load($file, $type = null) + public function load($file, string $type = null) { $path = $this->locator->locate($file); $this->setCurrentDir(\dirname($path)); @@ -47,8 +49,7 @@ public function load($file, $type = null) $result = $load($path); if (\is_object($result) && \is_callable($result)) { - $collection = new RouteCollection(); - $result(new RoutingConfigurator($collection, $this, $path, $file)); + $collection = $this->callConfigurator($result, $path, $file); } else { $collection = $result; } @@ -61,10 +62,19 @@ public function load($file, $type = null) /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type); } + + protected function callConfigurator(callable $result, string $path, string $file): RouteCollection + { + $collection = new RouteCollection(); + + $result(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); + + return $collection; + } } /** diff --git a/symfony/routing/Loader/XmlFileLoader.php b/symfony/routing/Loader/XmlFileLoader.php index 2f087793b..220153364 100644 --- a/symfony/routing/Loader/XmlFileLoader.php +++ b/symfony/routing/Loader/XmlFileLoader.php @@ -14,9 +14,10 @@ use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Util\XmlUtils; -use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\RouteCompiler; /** * XmlFileLoader loads XML routing files. @@ -26,6 +27,10 @@ */ class XmlFileLoader extends FileLoader { + use HostTrait; + use LocalizedRouteTrait; + use PrefixTrait; + public const NAMESPACE_URI = 'http://symfony.com/schema/routing'; public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd'; @@ -35,12 +40,12 @@ class XmlFileLoader extends FileLoader * @param string $file An XML file path * @param string|null $type The resource type * - * @return RouteCollection A RouteCollection instance + * @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, $type = null) + public function load($file, string $type = null) { $path = $this->locator->locate($file); @@ -64,13 +69,9 @@ public function load($file, $type = null) /** * Parses a node from a loaded XML file. * - * @param \DOMElement $node Element to parse - * @param string $path Full path of the XML file being processed - * @param string $file Loaded file name - * * @throws \InvalidArgumentException When the XML is invalid */ - protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file) + protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file) { if (self::NAMESPACE_URI !== $node->namespaceURI) { return; @@ -83,6 +84,16 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, $pa case 'import': $this->parseImport($collection, $node, $path, $file); break; + case 'when': + if (!$this->env || $node->getAttribute('env') !== $this->env) { + break; + } + foreach ($node->childNodes as $node) { + if ($node instanceof \DOMElement) { + $this->parseNode($collection, $node, $path, $file); + } + } + break; default: throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path)); } @@ -91,7 +102,7 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, $pa /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type); } @@ -99,21 +110,28 @@ public function supports($resource, $type = null) /** * Parses a route and adds it to the RouteCollection. * - * @param \DOMElement $node Element to parse that represents a Route - * @param string $path Full path of the XML file being processed - * * @throws \InvalidArgumentException When the XML is invalid */ - protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path) + protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path) { if ('' === $id = $node->getAttribute('id')) { throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" attribute.', $path)); } + if ('' !== $alias = $node->getAttribute('alias')) { + $alias = $collection->addAlias($id, $alias); + + if ($deprecationInfo = $this->parseDeprecation($node, $path)) { + $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']); + } + + return; + } + $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY); $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY); - [$defaults, $requirements, $options, $condition, $paths] = $this->parseConfigs($node, $path); + [$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path); if (!$paths && '' === $node->getAttribute('path')) { throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "path" attribute or child nodes.', $path)); @@ -123,30 +141,25 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $node, $p throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "path" attribute and child nodes.', $path)); } - if (!$paths) { - $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); - $collection->add($id, $route); - } else { - foreach ($paths as $locale => $p) { - $defaults['_locale'] = $locale; - $defaults['_canonical_route'] = $id; - $requirements['_locale'] = preg_quote($locale, RouteCompiler::REGEX_DELIMITER); - $route = new Route($p, $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); - $collection->add($id.'.'.$locale, $route); - } + $routes = $this->createLocalizedRoute($collection, $id, $paths ?: $node->getAttribute('path')); + $routes->addDefaults($defaults); + $routes->addRequirements($requirements); + $routes->addOptions($options); + $routes->setSchemes($schemes); + $routes->setMethods($methods); + $routes->setCondition($condition); + + if (null !== $hosts) { + $this->addHost($routes, $hosts); } } /** * Parses an import and adds the routes in the resource to the RouteCollection. * - * @param \DOMElement $node Element to parse that represents a Route - * @param string $path Full path of the XML file being processed - * @param string $file Loaded file name - * * @throws \InvalidArgumentException When the XML is invalid */ - protected function parseImport(RouteCollection $collection, \DOMElement $node, $path, $file) + 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)); @@ -154,12 +167,12 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ $type = $node->getAttribute('type'); $prefix = $node->getAttribute('prefix'); - $host = $node->hasAttribute('host') ? $node->getAttribute('host') : null; $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null; $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null; $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true; + $namePrefix = $node->getAttribute('name-prefix') ?: null; - [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes] = $this->parseConfigs($node, $path); + [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path); if ('' !== $prefix && $prefixes) { throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "prefix" attribute and child nodes.', $path)); @@ -182,51 +195,19 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ $this->setCurrentDir(\dirname($path)); /** @var RouteCollection[] $imported */ - $imported = $this->import($resource, ('' !== $type ? $type : null), false, $file, $exclude) ?: []; + $imported = $this->import($resource, '' !== $type ? $type : null, false, $file, $exclude) ?: []; if (!\is_array($imported)) { $imported = [$imported]; } foreach ($imported as $subCollection) { - /* @var $subCollection RouteCollection */ - if ('' !== $prefix || !$prefixes) { - $subCollection->addPrefix($prefix); - if (!$trailingSlashOnRoot) { - $rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath(); - foreach ($subCollection->all() as $route) { - if ($route->getPath() === $rootPath) { - $route->setPath(rtrim($rootPath, '/')); - } - } - } - } else { - foreach ($prefixes as $locale => $localePrefix) { - $prefixes[$locale] = trim(trim($localePrefix), '/'); - } - foreach ($subCollection->all() as $name => $route) { - if (null === $locale = $route->getDefault('_locale')) { - $subCollection->remove($name); - foreach ($prefixes as $locale => $localePrefix) { - $localizedRoute = clone $route; - $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $localizedRoute->setDefault('_locale', $locale); - $localizedRoute->setRequirement('_locale', preg_quote($locale, RouteCompiler::REGEX_DELIMITER)); - $localizedRoute->setDefault('_canonical_route', $name); - $subCollection->add($name.'.'.$locale, $localizedRoute); - } - } elseif (!isset($prefixes[$locale])) { - throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $path)); - } else { - $route->setPath($prefixes[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $subCollection->add($name, $route); - } - } - } + $this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot); - if (null !== $host) { - $subCollection->setHost($host); + if (null !== $hosts) { + $this->addHost($subCollection, $hosts); } + if (null !== $condition) { $subCollection->setCondition($condition); } @@ -236,30 +217,25 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ if (null !== $methods) { $subCollection->setMethods($methods); } + if (null !== $namePrefix) { + $subCollection->addNamePrefix($namePrefix); + } $subCollection->addDefaults($defaults); $subCollection->addRequirements($requirements); $subCollection->addOptions($options); - if ($namePrefix = $node->getAttribute('name-prefix')) { - $subCollection->addNamePrefix($namePrefix); - } - $collection->addCollection($subCollection); } } /** - * Loads an XML file. - * - * @param string $file An XML file path - * * @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($file) + protected function loadFile(string $file) { return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH); } @@ -277,6 +253,7 @@ private function parseConfigs(\DOMElement $node, string $path): array $condition = null; $prefixes = []; $paths = []; + $hosts = []; /** @var \DOMElement $n */ foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) { @@ -288,6 +265,9 @@ private function parseConfigs(\DOMElement $node, string $path): array case 'path': $paths[$n->getAttribute('locale')] = trim($n->textContent); break; + case 'host': + $hosts[$n->getAttribute('locale')] = trim($n->textContent); + break; case 'prefix': $prefixes[$n->getAttribute('locale')] = trim($n->textContent); break; @@ -331,14 +311,27 @@ private function parseConfigs(\DOMElement $node, string $path): array if ($node->hasAttribute('utf8')) { $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8')); } + if ($stateless = $node->getAttribute('stateless')) { + if (isset($defaults['_stateless'])) { + $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName); + + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name); + } - return [$defaults, $requirements, $options, $condition, $paths, $prefixes]; + $defaults['_stateless'] = XmlUtils::phpize($stateless); + } + + if (!$hosts) { + $hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null; + } + + return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts]; } /** * Parses the "default" elements. * - * @return array|bool|float|int|string|null The parsed value of the "default" element + * @return array|bool|float|int|string|null */ private function parseDefaultsConfig(\DOMElement $element, string $path) { @@ -370,7 +363,7 @@ private function parseDefaultsConfig(\DOMElement $element, string $path) /** * Recursively parses the value of a "default" element. * - * @return array|bool|float|int|string|null The parsed value + * @return array|bool|float|int|string|null * * @throws \InvalidArgumentException when the XML is invalid */ @@ -436,4 +429,41 @@ private function isElementValueNull(\DOMElement $element): bool return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil'); } + + /** + * Parses the deprecation elements. + * + * @throws \InvalidArgumentException When the XML is invalid + */ + private function parseDeprecation(\DOMElement $node, string $path): array + { + $deprecatedNode = null; + foreach ($node->childNodes as $child) { + if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) { + continue; + } + if ('deprecated' !== $child->localName) { + throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path)); + } + + $deprecatedNode = $child; + } + + if (null === $deprecatedNode) { + return []; + } + + if (!$deprecatedNode->hasAttribute('package')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "package" attribute.', $path)); + } + if (!$deprecatedNode->hasAttribute('version')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "version" attribute.', $path)); + } + + return [ + 'package' => $deprecatedNode->getAttribute('package'), + 'version' => $deprecatedNode->getAttribute('version'), + 'message' => trim($deprecatedNode->nodeValue), + ]; + } } diff --git a/symfony/routing/Loader/YamlFileLoader.php b/symfony/routing/Loader/YamlFileLoader.php index a4edb8869..ae98a314e 100644 --- a/symfony/routing/Loader/YamlFileLoader.php +++ b/symfony/routing/Loader/YamlFileLoader.php @@ -13,9 +13,10 @@ use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\RouteCompiler; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Yaml; @@ -28,8 +29,12 @@ */ class YamlFileLoader extends FileLoader { + use HostTrait; + use LocalizedRouteTrait; + use PrefixTrait; + 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', + '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; @@ -39,11 +44,11 @@ class YamlFileLoader extends FileLoader * @param string $file A Yaml file path * @param string|null $type The resource type * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection * * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid */ - public function load($file, $type = null) + public function load($file, string $type = null) { $path = $this->locator->locate($file); @@ -79,6 +84,24 @@ public function load($file, $type = null) } foreach ($parsedConfig as $name => $config) { + if (0 === strpos($name, 'when@')) { + if (!$this->env || 'when@'.$this->env !== $name) { + continue; + } + + foreach ($config as $name => $config) { + $this->validate($config, $name.'" when "@'.$this->env, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + + continue; + } + $this->validate($config, $name, $path); if (isset($config['resource'])) { @@ -94,31 +117,37 @@ public function load($file, $type = null) /** * {@inheritdoc} */ - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { 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. - * - * @param string $name Route name - * @param array $config Route definition - * @param string $path Full path of the YAML file being processed */ - protected function parseRoute(RouteCollection $collection, $name, array $config, $path) + protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path) { + if (isset($config['alias'])) { + $alias = $collection->addAlias($name, $config['alias']); + $deprecation = $config['deprecated'] ?? null; + if (null !== $deprecation) { + $alias->setDeprecated( + $deprecation['package'], + $deprecation['version'], + $deprecation['message'] ?? '' + ); + } + + return; + } + $defaults = $config['defaults'] ?? []; $requirements = $config['requirements'] ?? []; $options = $config['options'] ?? []; - $host = $config['host'] ?? ''; - $schemes = $config['schemes'] ?? []; - $methods = $config['methods'] ?? []; - $condition = $config['condition'] ?? null; foreach ($requirements as $placeholder => $requirement) { if (\is_int($placeholder)) { - @trigger_error(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"?', $placeholder, $requirement, $name, $path), \E_USER_DEPRECATED); + 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"?', $placeholder, $requirement, $name, $path)); } } @@ -134,32 +163,27 @@ protected function parseRoute(RouteCollection $collection, $name, array $config, if (isset($config['utf8'])) { $options['utf8'] = $config['utf8']; } + if (isset($config['stateless'])) { + $defaults['_stateless'] = $config['stateless']; + } - if (\is_array($config['path'])) { - $route = new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $routes = $this->createLocalizedRoute($collection, $name, $config['path']); + $routes->addDefaults($defaults); + $routes->addRequirements($requirements); + $routes->addOptions($options); + $routes->setSchemes($config['schemes'] ?? []); + $routes->setMethods($config['methods'] ?? []); + $routes->setCondition($config['condition'] ?? null); - foreach ($config['path'] as $locale => $path) { - $localizedRoute = clone $route; - $localizedRoute->setDefault('_locale', $locale); - $localizedRoute->setRequirement('_locale', preg_quote($locale, RouteCompiler::REGEX_DELIMITER)); - $localizedRoute->setDefault('_canonical_route', $name); - $localizedRoute->setPath($path); - $collection->add($name.'.'.$locale, $localizedRoute); - } - } else { - $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); - $collection->add($name, $route); + if (isset($config['host'])) { + $this->addHost($routes, $config['host']); } } /** * Parses an import and adds the routes in the resource to the RouteCollection. - * - * @param array $config Route definition - * @param string $path Full path of the YAML file being processed - * @param string $file Loaded file name */ - protected function parseImport(RouteCollection $collection, array $config, $path, $file) + protected function parseImport(RouteCollection $collection, array $config, string $path, string $file) { $type = $config['type'] ?? null; $prefix = $config['prefix'] ?? ''; @@ -171,6 +195,7 @@ protected function parseImport(RouteCollection $collection, array $config, $path $schemes = $config['schemes'] ?? null; $methods = $config['methods'] ?? null; $trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true; + $namePrefix = $config['name_prefix'] ?? null; $exclude = $config['exclude'] ?? null; if (isset($config['controller'])) { @@ -185,9 +210,13 @@ protected function parseImport(RouteCollection $collection, array $config, $path if (isset($config['utf8'])) { $options['utf8'] = $config['utf8']; } + if (isset($config['stateless'])) { + $defaults['_stateless'] = $config['stateless']; + } $this->setCurrentDir(\dirname($path)); + /** @var RouteCollection[] $imported */ $imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: []; if (!\is_array($imported)) { @@ -195,43 +224,10 @@ protected function parseImport(RouteCollection $collection, array $config, $path } foreach ($imported as $subCollection) { - /* @var $subCollection RouteCollection */ - if (!\is_array($prefix)) { - $subCollection->addPrefix($prefix); - if (!$trailingSlashOnRoot) { - $rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath(); - foreach ($subCollection->all() as $route) { - if ($route->getPath() === $rootPath) { - $route->setPath(rtrim($rootPath, '/')); - } - } - } - } else { - foreach ($prefix as $locale => $localePrefix) { - $prefix[$locale] = trim(trim($localePrefix), '/'); - } - foreach ($subCollection->all() as $name => $route) { - if (null === $locale = $route->getDefault('_locale')) { - $subCollection->remove($name); - foreach ($prefix as $locale => $localePrefix) { - $localizedRoute = clone $route; - $localizedRoute->setDefault('_locale', $locale); - $localizedRoute->setRequirement('_locale', preg_quote($locale, RouteCompiler::REGEX_DELIMITER)); - $localizedRoute->setDefault('_canonical_route', $name); - $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $subCollection->add($name.'.'.$locale, $localizedRoute); - } - } elseif (!isset($prefix[$locale])) { - throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $file)); - } else { - $route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); - $subCollection->add($name, $route); - } - } - } + $this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot); if (null !== $host) { - $subCollection->setHost($host); + $this->addHost($subCollection, $host); } if (null !== $condition) { $subCollection->setCondition($condition); @@ -242,14 +238,13 @@ protected function parseImport(RouteCollection $collection, array $config, $path if (null !== $methods) { $subCollection->setMethods($methods); } + if (null !== $namePrefix) { + $subCollection->addNamePrefix($namePrefix); + } $subCollection->addDefaults($defaults); $subCollection->addRequirements($requirements); $subCollection->addOptions($options); - if (isset($config['name_prefix'])) { - $subCollection->addNamePrefix($config['name_prefix']); - } - $collection->addCollection($subCollection); } } @@ -264,11 +259,16 @@ protected function parseImport(RouteCollection $collection, array $config, $path * @throws \InvalidArgumentException If one of the provided config keys is not supported, * something is missing or the combination is nonsense */ - protected function validate($config, $name, $path) + protected function validate($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)); } + if (isset($config['alias'])) { + $this->validateAlias($config, $name, $path); + + return; + } if ($extraKeys = array_diff(array_keys($config), self::AVAILABLE_KEYS)) { throw new \InvalidArgumentException(sprintf('The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', $path, $name, implode('", "', $extraKeys), implode('", "', self::AVAILABLE_KEYS))); } @@ -284,5 +284,31 @@ protected function validate($config, $name, $path) if (isset($config['controller']) && isset($config['defaults']['_controller'])) { throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name)); } + if (isset($config['stateless']) && isset($config['defaults']['_stateless'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" key and the defaults key "_stateless" for "%s".', $path, $name)); + } + } + + /** + * @throws \InvalidArgumentException If one of the provided config keys is not supported, + * something is missing or the combination is nonsense + */ + private function validateAlias(array $config, string $name, string $path): void + { + foreach ($config as $key => $value) { + if (!\in_array($key, ['alias', 'deprecated'], true)) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify other keys than "alias" and "deprecated" for "%s".', $path, $name)); + } + + if ('deprecated' === $key) { + if (!isset($value['package'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "package" of the "deprecated" option for "%s".', $path, $name)); + } + + if (!isset($value['version'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "version" of the "deprecated" option for "%s".', $path, $name)); + } + } + } } } diff --git a/symfony/routing/Loader/schema/routing/routing-1.0.xsd b/symfony/routing/Loader/schema/routing/routing-1.0.xsd index 8e61d03e9..66c40a0d8 100644 --- a/symfony/routing/Loader/schema/routing/routing-1.0.xsd +++ b/symfony/routing/Loader/schema/routing/routing-1.0.xsd @@ -21,9 +21,18 @@ + + + + + + + + + @@ -45,6 +54,8 @@ + + @@ -55,6 +66,8 @@ + + @@ -62,6 +75,7 @@ + @@ -76,6 +90,7 @@ + @@ -167,4 +182,13 @@ + + + + + + + + + diff --git a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php index 123130ed4..4b3888045 100644 --- a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -139,7 +139,8 @@ private function generateCompiledRoutes(): string foreach ($staticRoutes as $path => $routes) { $code .= sprintf(" %s => [\n", self::export($path)); foreach ($routes as $route) { - $code .= sprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", ...array_map([__CLASS__, 'export'], $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 .= " ],\n"; } @@ -151,7 +152,8 @@ private function generateCompiledRoutes(): string foreach ($dynamicRoutes as $path => $routes) { $code .= sprintf(" %s => [\n", self::export($path)); foreach ($routes as $route) { - $code .= sprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", ...array_map([__CLASS__, 'export'], $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 .= " ],\n"; } diff --git a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php index f78a6cb72..bdb7ba3d0 100644 --- a/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +++ b/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php @@ -30,9 +30,13 @@ trait CompiledUrlMatcherTrait private $staticRoutes = []; private $regexpList = []; private $dynamicRoutes = []; + + /** + * @var callable|null + */ private $checkCondition; - public function match($pathinfo): array + public function match(string $pathinfo): array { $allow = $allowSchemes = []; if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) { @@ -93,10 +97,10 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche } if ($requiredHost) { - if ('#' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) { + if ('{' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) { continue; } - if ('#' === $requiredHost[0] && $hostMatches) { + if ('{' === $requiredHost[0] && $hostMatches) { $hostMatches['_route'] = $ret['_route']; $ret = $this->mergeDefaults($hostMatches, $ret); } diff --git a/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php b/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php index 34aad9274..8e33802d3 100644 --- a/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php +++ b/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php @@ -24,14 +24,14 @@ 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 Executable code + * @return string */ public function dump(array $options = []); /** * Gets the routes to dump. * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection */ public function getRoutes(); } diff --git a/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php b/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php deleted file mode 100644 index 09404efaa..000000000 --- a/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php +++ /dev/null @@ -1,75 +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\Matcher\Dumper; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "CompiledUrlMatcherDumper" instead.', PhpMatcherDumper::class), \E_USER_DEPRECATED); - -/** - * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. - * - * @author Fabien Potencier - * @author Tobias Schultze - * @author Arnaud Le Blanc - * @author Nicolas Grekas - * - * @deprecated since Symfony 4.3, use CompiledUrlMatcherDumper instead. - */ -class PhpMatcherDumper extends CompiledUrlMatcherDumper -{ - /** - * Dumps a set of routes to a PHP class. - * - * Available options: - * - * * class: The class name - * * base_class: The base class name - * - * @param array $options An array of options - * - * @return string A PHP class representing the matcher class - */ - public function dump(array $options = []) - { - $options = array_replace([ - 'class' => 'ProjectUrlMatcher', - 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', - ], $options); - - $code = parent::dump(); - $code = preg_replace('#\n ([^ ].*?) // \$(\w++)$#m', "\n \$this->$2 = $1", $code); - $code = str_replace(",\n $", ";\n $", $code); - $code = substr($code, strpos($code, '$this') - 4, -5).";\n"; - $code = preg_replace('/^ \$this->\w++ = (?:null|false|\[\n \]);\n/m', '', $code); - $code = str_replace("\n ", "\n ", "\n".$code); - - return <<context = \$context;{$code} } -} - -EOF; - } -} diff --git a/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php b/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php index d61282bd3..97bd692a5 100644 --- a/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php +++ b/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php @@ -151,40 +151,43 @@ private function getCommonPrefix(string $prefix, string $anotherPrefix): array $staticLength = null; set_error_handler([__CLASS__, 'handleError']); - for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) { - if ('(' === $prefix[$i]) { - $staticLength = $staticLength ?? $i; - for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) { - if ($prefix[$j] !== $anotherPrefix[$j]) { - break 2; + try { + for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) { + if ('(' === $prefix[$i]) { + $staticLength = $staticLength ?? $i; + for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) { + if ($prefix[$j] !== $anotherPrefix[$j]) { + break 2; + } + if ('(' === $prefix[$j]) { + ++$n; + } elseif (')' === $prefix[$j]) { + --$n; + } elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) { + --$j; + break; + } } - if ('(' === $prefix[$j]) { - ++$n; - } elseif (')' === $prefix[$j]) { - --$n; - } elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) { - --$j; + if (0 < $n) { break; } - } - if (0 < $n) { - break; - } - if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) { - break; - } - $subPattern = substr($prefix, $i, $j - $i); - if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) { do { // Prevent cutting in the middle of an UTF-8 characters diff --git a/symfony/routing/Matcher/ExpressionLanguageProvider.php b/symfony/routing/Matcher/ExpressionLanguageProvider.php new file mode 100644 index 000000000..96bb7babf --- /dev/null +++ b/symfony/routing/Matcher/ExpressionLanguageProvider.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use Symfony\Contracts\Service\ServiceProviderInterface; + +/** + * Exposes functions defined in the request context to route conditions. + * + * @author Ahmed TAILOULOUTE + */ +class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface +{ + private $functions; + + public function __construct(ServiceProviderInterface $functions) + { + $this->functions = $functions; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + $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); + } + ); + } + + return $functions; + } + + public function get(string $function): callable + { + return $this->functions->get($function); + } +} diff --git a/symfony/routing/Matcher/RedirectableUrlMatcher.php b/symfony/routing/Matcher/RedirectableUrlMatcher.php index eb7bec7f3..3cd7c81a6 100644 --- a/symfony/routing/Matcher/RedirectableUrlMatcher.php +++ b/symfony/routing/Matcher/RedirectableUrlMatcher.php @@ -22,7 +22,7 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable /** * {@inheritdoc} */ - public function match($pathinfo) + public function match(string $pathinfo) { try { return parent::match($pathinfo); diff --git a/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php b/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php index 7c27bc879..d07f42093 100644 --- a/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php +++ b/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php @@ -19,13 +19,13 @@ interface RedirectableUrlMatcherInterface { /** - * Redirects the user to another URL. + * Redirects the user to another URL and returns the parameters for the redirection. * * @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 An array of parameters + * @return array */ - public function redirect($path, $route, $scheme = null); + public function redirect(string $path, string $route, string $scheme = null); } diff --git a/symfony/routing/Matcher/RequestMatcherInterface.php b/symfony/routing/Matcher/RequestMatcherInterface.php index 0c193ff2d..c05016e82 100644 --- a/symfony/routing/Matcher/RequestMatcherInterface.php +++ b/symfony/routing/Matcher/RequestMatcherInterface.php @@ -26,10 +26,10 @@ interface RequestMatcherInterface /** * Tries to match a request with a set of routes. * - * If the matcher can not find information, it must throw one of the exceptions documented + * If the matcher cannot find information, it must throw one of the exceptions documented * below. * - * @return array An array of parameters + * @return array * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If no matching resource could be found diff --git a/symfony/routing/Matcher/TraceableUrlMatcher.php b/symfony/routing/Matcher/TraceableUrlMatcher.php index f113eb03b..9e8c4c42d 100644 --- a/symfony/routing/Matcher/TraceableUrlMatcher.php +++ b/symfony/routing/Matcher/TraceableUrlMatcher.php @@ -29,7 +29,7 @@ class TraceableUrlMatcher extends UrlMatcher protected $traces; - public function getTraces($pathinfo) + public function getTraces(string $pathinfo) { $this->traces = []; @@ -50,7 +50,7 @@ public function getTracesForRequest(Request $request) return $traces; } - protected function matchCollection($pathinfo, RouteCollection $routes) + protected function matchCollection(string $pathinfo, RouteCollection $routes) { // HEAD and GET are equivalent as per RFC if ('HEAD' === $method = $this->context->getMethod()) { diff --git a/symfony/routing/Matcher/UrlMatcher.php b/symfony/routing/Matcher/UrlMatcher.php index a14b04faa..f076a2f5e 100644 --- a/symfony/routing/Matcher/UrlMatcher.php +++ b/symfony/routing/Matcher/UrlMatcher.php @@ -81,7 +81,7 @@ public function getContext() /** * {@inheritdoc} */ - public function match($pathinfo) + public function match(string $pathinfo) { $this->allow = $this->allowSchemes = []; @@ -120,13 +120,13 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac * * @param string $pathinfo The path info to be parsed * - * @return array An array of parameters + * @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($pathinfo, RouteCollection $routes) + protected function matchCollection(string $pathinfo, RouteCollection $routes) { // HEAD and GET are equivalent as per RFC if ('HEAD' === $method = $this->context->getMethod()) { @@ -205,12 +205,9 @@ protected function matchCollection($pathinfo, RouteCollection $routes) * in matchers that do not have access to the matched Route instance * (like the PHP and Apache matcher dumpers). * - * @param string $name The name of the route - * @param array $attributes An array of attributes from the matcher - * - * @return array An array of parameters + * @return array */ - protected function getAttributes(Route $route, $name, array $attributes) + protected function getAttributes(Route $route, string $name, array $attributes) { $defaults = $route->getDefaults(); if (isset($defaults['_canonical_route'])) { @@ -225,12 +222,9 @@ protected function getAttributes(Route $route, $name, array $attributes) /** * Handles specific route requirements. * - * @param string $pathinfo The path - * @param string $name The route name - * * @return array The first element represents the status, the second contains additional information */ - protected function handleRouteRequirements($pathinfo, $name, Route $route) + protected function handleRouteRequirements(string $pathinfo, string $name, Route $route) { // expression condition if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { @@ -243,12 +237,9 @@ protected function handleRouteRequirements($pathinfo, $name, Route $route) /** * Get merged default parameters. * - * @param array $params The parameters - * @param array $defaults The defaults - * - * @return array Merged default parameters + * @return array */ - protected function mergeDefaults($params, $defaults) + protected function mergeDefaults(array $params, array $defaults) { foreach ($params as $key => $value) { if (!\is_int($key) && null !== $value) { diff --git a/symfony/routing/Matcher/UrlMatcherInterface.php b/symfony/routing/Matcher/UrlMatcherInterface.php index 17f1f97b3..0a5be9744 100644 --- a/symfony/routing/Matcher/UrlMatcherInterface.php +++ b/symfony/routing/Matcher/UrlMatcherInterface.php @@ -26,16 +26,16 @@ interface UrlMatcherInterface extends RequestContextAwareInterface /** * Tries to match a URL path with a set of routes. * - * If the matcher can not find information, it must throw one of the exceptions documented + * If the matcher cannot find information, it must throw one of the exceptions documented * below. * * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded) * - * @return array An array of parameters + * @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($pathinfo); + public function match(string $pathinfo); } diff --git a/symfony/routing/RequestContext.php b/symfony/routing/RequestContext.php index b1982a63f..f54c430ee 100644 --- a/symfony/routing/RequestContext.php +++ b/symfony/routing/RequestContext.php @@ -45,6 +45,23 @@ public function __construct(string $baseUrl = '', string $method = 'GET', string $this->setQueryString($queryString); } + public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self + { + $uri = parse_url($uri); + $scheme = $uri['scheme'] ?? $scheme; + $host = $uri['host'] ?? $host; + + if (isset($uri['port'])) { + if ('http' === $scheme) { + $httpPort = $uri['port']; + } elseif ('https' === $scheme) { + $httpsPort = $uri['port']; + } + } + + return new self($uri['path'] ?? '', 'GET', $host, $scheme, $httpPort, $httpsPort); + } + /** * Updates the RequestContext information based on a HttpFoundation Request. * @@ -67,7 +84,7 @@ public function fromRequest(Request $request) /** * Gets the base URL. * - * @return string The base URL + * @return string */ public function getBaseUrl() { @@ -77,13 +94,11 @@ public function getBaseUrl() /** * Sets the base URL. * - * @param string $baseUrl The base URL - * * @return $this */ - public function setBaseUrl($baseUrl) + public function setBaseUrl(string $baseUrl) { - $this->baseUrl = $baseUrl; + $this->baseUrl = rtrim($baseUrl, '/'); return $this; } @@ -91,7 +106,7 @@ public function setBaseUrl($baseUrl) /** * Gets the path info. * - * @return string The path info + * @return string */ public function getPathInfo() { @@ -101,11 +116,9 @@ public function getPathInfo() /** * Sets the path info. * - * @param string $pathInfo The path info - * * @return $this */ - public function setPathInfo($pathInfo) + public function setPathInfo(string $pathInfo) { $this->pathInfo = $pathInfo; @@ -117,7 +130,7 @@ public function setPathInfo($pathInfo) * * The method is always an uppercased string. * - * @return string The HTTP method + * @return string */ public function getMethod() { @@ -127,11 +140,9 @@ public function getMethod() /** * Sets the HTTP method. * - * @param string $method The HTTP method - * * @return $this */ - public function setMethod($method) + public function setMethod(string $method) { $this->method = strtoupper($method); @@ -143,7 +154,7 @@ public function setMethod($method) * * The host is always lowercased because it must be treated case-insensitive. * - * @return string The HTTP host + * @return string */ public function getHost() { @@ -153,11 +164,9 @@ public function getHost() /** * Sets the HTTP host. * - * @param string $host The HTTP host - * * @return $this */ - public function setHost($host) + public function setHost(string $host) { $this->host = strtolower($host); @@ -167,7 +176,7 @@ public function setHost($host) /** * Gets the HTTP scheme. * - * @return string The HTTP scheme + * @return string */ public function getScheme() { @@ -177,11 +186,9 @@ public function getScheme() /** * Sets the HTTP scheme. * - * @param string $scheme The HTTP scheme - * * @return $this */ - public function setScheme($scheme) + public function setScheme(string $scheme) { $this->scheme = strtolower($scheme); @@ -191,7 +198,7 @@ public function setScheme($scheme) /** * Gets the HTTP port. * - * @return int The HTTP port + * @return int */ public function getHttpPort() { @@ -201,13 +208,11 @@ public function getHttpPort() /** * Sets the HTTP port. * - * @param int $httpPort The HTTP port - * * @return $this */ - public function setHttpPort($httpPort) + public function setHttpPort(int $httpPort) { - $this->httpPort = (int) $httpPort; + $this->httpPort = $httpPort; return $this; } @@ -215,7 +220,7 @@ public function setHttpPort($httpPort) /** * Gets the HTTPS port. * - * @return int The HTTPS port + * @return int */ public function getHttpsPort() { @@ -225,21 +230,19 @@ public function getHttpsPort() /** * Sets the HTTPS port. * - * @param int $httpsPort The HTTPS port - * * @return $this */ - public function setHttpsPort($httpsPort) + public function setHttpsPort(int $httpsPort) { - $this->httpsPort = (int) $httpsPort; + $this->httpsPort = $httpsPort; return $this; } /** - * Gets the query string. + * Gets the query string without the "?". * - * @return string The query string without the "?" + * @return string */ public function getQueryString() { @@ -249,11 +252,9 @@ public function getQueryString() /** * Sets the query string. * - * @param string $queryString The query string (after "?") - * * @return $this */ - public function setQueryString($queryString) + public function setQueryString(?string $queryString) { // string cast to be fault-tolerant, accepting null $this->queryString = (string) $queryString; @@ -264,7 +265,7 @@ public function setQueryString($queryString) /** * Returns the parameters. * - * @return array The parameters + * @return array */ public function getParameters() { @@ -288,11 +289,9 @@ public function setParameters(array $parameters) /** * Gets a parameter value. * - * @param string $name A parameter name - * - * @return mixed The parameter value or null if nonexistent + * @return mixed */ - public function getParameter($name) + public function getParameter(string $name) { return $this->parameters[$name] ?? null; } @@ -300,11 +299,9 @@ public function getParameter($name) /** * Checks if a parameter value is set for the given parameter. * - * @param string $name A parameter name - * - * @return bool True if the parameter value is set, false otherwise + * @return bool */ - public function hasParameter($name) + public function hasParameter(string $name) { return \array_key_exists($name, $this->parameters); } @@ -312,15 +309,19 @@ public function hasParameter($name) /** * Sets a parameter value. * - * @param string $name A parameter name - * @param mixed $parameter The parameter value + * @param mixed $parameter The parameter value * * @return $this */ - public function setParameter($name, $parameter) + public function setParameter(string $name, $parameter) { $this->parameters[$name] = $parameter; return $this; } + + public function isSecure(): bool + { + return 'https' === $this->scheme; + } } diff --git a/symfony/routing/RequestContextAwareInterface.php b/symfony/routing/RequestContextAwareInterface.php index df5b9fcd4..270a2b084 100644 --- a/symfony/routing/RequestContextAwareInterface.php +++ b/symfony/routing/RequestContextAwareInterface.php @@ -21,7 +21,7 @@ public function setContext(RequestContext $context); /** * Gets the request context. * - * @return RequestContext The context + * @return RequestContext */ public function getContext(); } diff --git a/symfony/routing/Route.php b/symfony/routing/Route.php index baabe0d02..c67bd2de5 100644 --- a/symfony/routing/Route.php +++ b/symfony/routing/Route.php @@ -78,12 +78,9 @@ public function __serialize(): array } /** - * @return string - * - * @internal since Symfony 4.3 - * @final since Symfony 4.3 + * @internal */ - public function serialize() + final public function serialize(): string { return serialize($this->__serialize()); } @@ -107,16 +104,15 @@ public function __unserialize(array $data): void } /** - * @internal since Symfony 4.3 - * @final since Symfony 4.3 + * @internal */ - public function unserialize($serialized) + final public function unserialize($serialized) { $this->__unserialize(unserialize($serialized)); } /** - * @return string The path pattern + * @return string */ public function getPath() { @@ -124,26 +120,11 @@ public function getPath() } /** - * Sets the pattern for the path. - * - * @param string $pattern The path pattern - * * @return $this */ - public function setPath($pattern) + public function setPath(string $pattern) { - if (false !== strpbrk($pattern, '?<')) { - $pattern = preg_replace_callback('#\{(!?)(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) { - if (isset($m[4][0])) { - $this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null); - } - if (isset($m[3][0])) { - $this->setRequirement($m[2], substr($m[3], 1, -1)); - } - - return '{'.$m[1].$m[2].'}'; - }, $pattern); - } + $pattern = $this->extractInlineDefaultsAndRequirements($pattern); // A pattern must start with a slash and must not have multiple slashes at the beginning because the // generated path for this route would be confused with a network path, e.g. '//domain.com/path'. @@ -154,7 +135,7 @@ public function setPath($pattern) } /** - * @return string The host pattern + * @return string */ public function getHost() { @@ -162,15 +143,11 @@ public function getHost() } /** - * Sets the pattern for the host. - * - * @param string $pattern The host pattern - * * @return $this */ - public function setHost($pattern) + public function setHost(?string $pattern) { - $this->host = (string) $pattern; + $this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern); $this->compiled = null; return $this; @@ -180,7 +157,7 @@ public function setHost($pattern) * Returns the lowercased schemes this route is restricted to. * So an empty array means that any scheme is allowed. * - * @return string[] The schemes + * @return string[] */ public function getSchemes() { @@ -206,11 +183,9 @@ public function setSchemes($schemes) /** * Checks if a scheme requirement has been set. * - * @param string $scheme - * - * @return bool true if the scheme requirement exists, otherwise false + * @return bool */ - public function hasScheme($scheme) + public function hasScheme(string $scheme) { return \in_array(strtolower($scheme), $this->schemes, true); } @@ -219,7 +194,7 @@ public function hasScheme($scheme) * Returns the uppercased HTTP methods this route is restricted to. * So an empty array means that any method is allowed. * - * @return string[] The methods + * @return string[] */ public function getMethods() { @@ -243,7 +218,7 @@ public function setMethods($methods) } /** - * @return array The options + * @return array */ public function getOptions() { @@ -278,12 +253,11 @@ public function addOptions(array $options) /** * Sets an option value. * - * @param string $name An option name - * @param mixed $value The option value + * @param mixed $value The option value * * @return $this */ - public function setOption($name, $value) + public function setOption(string $name, $value) { $this->options[$name] = $value; $this->compiled = null; @@ -292,31 +266,25 @@ public function setOption($name, $value) } /** - * Get an option value. - * - * @param string $name An option name + * Returns the option value or null when not found. * - * @return mixed The option value or null when not given + * @return mixed */ - public function getOption($name) + public function getOption(string $name) { return $this->options[$name] ?? null; } /** - * Checks if an option has been set. - * - * @param string $name An option name - * - * @return bool true if the option is set, false otherwise + * @return bool */ - public function hasOption($name) + public function hasOption(string $name) { return \array_key_exists($name, $this->options); } /** - * @return array The defaults + * @return array */ public function getDefaults() { @@ -351,25 +319,17 @@ public function addDefaults(array $defaults) } /** - * Gets a default value. - * - * @param string $name A variable name - * - * @return mixed The default value or null when not given + * @return mixed */ - public function getDefault($name) + public function getDefault(string $name) { return $this->defaults[$name] ?? null; } /** - * Checks if a default value is set for the given variable. - * - * @param string $name A variable name - * - * @return bool true if the default value is set, false otherwise + * @return bool */ - public function hasDefault($name) + public function hasDefault(string $name) { return \array_key_exists($name, $this->defaults); } @@ -377,12 +337,11 @@ public function hasDefault($name) /** * Sets a default value. * - * @param string $name A variable name - * @param mixed $default The default value + * @param mixed $default The default value * * @return $this */ - public function setDefault($name, $default) + public function setDefault(string $name, $default) { if ('_locale' === $name && $this->isLocalized()) { return $this; @@ -395,7 +354,7 @@ public function setDefault($name, $default) } /** - * @return array The requirements + * @return array */ public function getRequirements() { @@ -430,38 +389,25 @@ public function addRequirements(array $requirements) } /** - * Returns the requirement for the given key. - * - * @param string $key The key - * - * @return string|null The regex or null when not given + * @return string|null */ - public function getRequirement($key) + public function getRequirement(string $key) { return $this->requirements[$key] ?? null; } /** - * Checks if a requirement is set for the given key. - * - * @param string $key A variable name - * - * @return bool true if a requirement is specified, false otherwise + * @return bool */ - public function hasRequirement($key) + public function hasRequirement(string $key) { return \array_key_exists($key, $this->requirements); } /** - * Sets a requirement for the given key. - * - * @param string $key The key - * @param string $regex The regex - * * @return $this */ - public function setRequirement($key, $regex) + public function setRequirement(string $key, string $regex) { if ('_locale' === $key && $this->isLocalized()) { return $this; @@ -474,7 +420,7 @@ public function setRequirement($key, $regex) } /** - * @return string The condition + * @return string */ public function getCondition() { @@ -482,13 +428,9 @@ public function getCondition() } /** - * Sets the condition. - * - * @param string $condition The condition - * * @return $this */ - public function setCondition($condition) + public function setCondition(?string $condition) { $this->condition = (string) $condition; $this->compiled = null; @@ -499,7 +441,7 @@ public function setCondition($condition) /** * Compiles the route. * - * @return CompiledRoute A CompiledRoute instance + * @return CompiledRoute * * @throws \LogicException If the Route cannot be compiled because the * path or host pattern is invalid @@ -517,18 +459,38 @@ public function compile() return $this->compiled = $class::compile($this); } - private function sanitizeRequirement(string $key, $regex) + private function extractInlineDefaultsAndRequirements(string $pattern): string { - if (!\is_string($regex)) { - throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" must be a string.', $key)); + if (false === strpbrk($pattern, '?<')) { + return $pattern; } - if ('' !== $regex && '^' === $regex[0]) { - $regex = (string) substr($regex, 1); // returns false for a single character + return preg_replace_callback('#\{(!?)(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) { + if (isset($m[4][0])) { + $this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null); + } + if (isset($m[3][0])) { + $this->setRequirement($m[2], substr($m[3], 1, -1)); + } + + return '{'.$m[1].$m[2].'}'; + }, $pattern); + } + + private function sanitizeRequirement(string $key, string $regex) + { + if ('' !== $regex) { + if ('^' === $regex[0]) { + $regex = substr($regex, 1); + } elseif (0 === strpos($regex, '\\A')) { + $regex = substr($regex, 2); + } } if (str_ends_with($regex, '$')) { $regex = substr($regex, 0, -1); + } elseif (\strlen($regex) - 2 === strpos($regex, '\\z')) { + $regex = substr($regex, 0, -2); } if ('' === $regex) { @@ -540,6 +502,6 @@ private function sanitizeRequirement(string $key, $regex) private function isLocalized(): bool { - return isset($this->defaults['_locale']) && isset($this->defaults['_canonical_route']) && ($this->requirements['_locale'] ?? null) === preg_quote($this->defaults['_locale'], RouteCompiler::REGEX_DELIMITER); + return isset($this->defaults['_locale']) && isset($this->defaults['_canonical_route']) && ($this->requirements['_locale'] ?? null) === preg_quote($this->defaults['_locale']); } } diff --git a/symfony/routing/RouteCollection.php b/symfony/routing/RouteCollection.php index 56e925cac..a0700bba3 100644 --- a/symfony/routing/RouteCollection.php +++ b/symfony/routing/RouteCollection.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Routing; use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Routing\Exception\InvalidArgumentException; +use Symfony\Component\Routing\Exception\RouteCircularReferenceException; /** * A RouteCollection represents a set of Route instances. @@ -22,24 +24,40 @@ * * @author Fabien Potencier * @author Tobias Schultze + * + * @implements \IteratorAggregate */ class RouteCollection implements \IteratorAggregate, \Countable { /** - * @var Route[] + * @var array */ private $routes = []; /** - * @var array + * @var array + */ + private $aliases = []; + + /** + * @var array */ private $resources = []; + /** + * @var array + */ + private $priorities = []; + public function __clone() { foreach ($this->routes as $name => $route) { $this->routes[$name] = clone $route; } + + foreach ($this->aliases as $name => $alias) { + $this->aliases[$name] = clone $alias; + } } /** @@ -49,18 +67,18 @@ public function __clone() * * @see all() * - * @return \ArrayIterator|Route[] An \ArrayIterator object for iterating over routes + * @return \ArrayIterator */ #[\ReturnTypeWillChange] public function getIterator() { - return new \ArrayIterator($this->routes); + return new \ArrayIterator($this->all()); } /** * Gets the number of Routes in this collection. * - * @return int The number of routes + * @return int */ #[\ReturnTypeWillChange] public function count() @@ -69,36 +87,66 @@ public function count() } /** - * Adds a route. - * - * @param string $name The route name + * @param int $priority */ - public function add($name, Route $route) + public function add(string $name, Route $route/* , int $priority = 0 */) { - unset($this->routes[$name]); + 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) { + $this->priorities[$name] = $priority; + } } /** * Returns all routes in this collection. * - * @return Route[] An array of routes + * @return array */ public function all() { + 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]); + }); + } + return $this->routes; } /** * Gets a route by name. * - * @param string $name The route name - * - * @return Route|null A Route instance or null when not found + * @return Route|null */ - public function get($name) + public function get(string $name) { + $visited = []; + while (null !== $alias = $this->aliases[$name] ?? null) { + if (false !== $searchKey = array_search($name, $visited)) { + $visited[] = $name; + + throw new RouteCircularReferenceException($name, \array_slice($visited, $searchKey)); + } + + if ($alias->isDeprecated()) { + $deprecation = $alias->getDeprecation($name); + + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } + + $visited[] = $name; + $name = $alias->getId(); + } + return $this->routes[$name] ?? null; } @@ -110,7 +158,7 @@ public function get($name) public function remove($name) { foreach ((array) $name as $n) { - unset($this->routes[$n]); + unset($this->routes[$n], $this->priorities[$n], $this->aliases[$n]); } } @@ -123,8 +171,18 @@ public function addCollection(self $collection) // we need to remove all routes with the same names first because just replacing them // would not place the new route at the end of the merged array foreach ($collection->all() as $name => $route) { - unset($this->routes[$name]); + unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); $this->routes[$name] = $route; + + if (isset($collection->priorities[$name])) { + $this->priorities[$name] = $collection->priorities[$name]; + } + } + + foreach ($collection->getAliases() as $name => $alias) { + unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); + + $this->aliases[$name] = $alias; } foreach ($collection->getResources() as $resource) { @@ -134,17 +192,9 @@ public function addCollection(self $collection) /** * Adds a prefix to the path of all child routes. - * - * @param string $prefix An optional prefix to add before each pattern of the route collection - * @param array $defaults An array of default values - * @param array $requirements An array of requirements */ - public function addPrefix($prefix, array $defaults = [], array $requirements = []) + public function addPrefix(string $prefix, array $defaults = [], array $requirements = []) { - if (null === $prefix) { - @trigger_error(sprintf('Passing null as $prefix to %s is deprecated in Symfony 4.4 and will trigger a TypeError in 5.0.', __METHOD__), \E_USER_DEPRECATED); - } - $prefix = trim(trim($prefix), '/'); if ('' === $prefix) { @@ -164,25 +214,32 @@ public function addPrefix($prefix, array $defaults = [], array $requirements = [ public function addNamePrefix(string $prefix) { $prefixedRoutes = []; + $prefixedPriorities = []; + $prefixedAliases = []; foreach ($this->routes as $name => $route) { $prefixedRoutes[$prefix.$name] = $route; - if (null !== $name = $route->getDefault('_canonical_route')) { - $route->setDefault('_canonical_route', $prefix.$name); + if (null !== $canonicalName = $route->getDefault('_canonical_route')) { + $route->setDefault('_canonical_route', $prefix.$canonicalName); } + if (isset($this->priorities[$name])) { + $prefixedPriorities[$prefix.$name] = $this->priorities[$name]; + } + } + + foreach ($this->aliases as $name => $alias) { + $prefixedAliases[$prefix.$name] = $alias->withId($prefix.$alias->getId()); } $this->routes = $prefixedRoutes; + $this->priorities = $prefixedPriorities; + $this->aliases = $prefixedAliases; } /** * Sets the host pattern on all routes. - * - * @param string $pattern The pattern - * @param array $defaults An array of default values - * @param array $requirements An array of requirements */ - public function setHost($pattern, array $defaults = [], array $requirements = []) + public function setHost(?string $pattern, array $defaults = [], array $requirements = []) { foreach ($this->routes as $route) { $route->setHost($pattern); @@ -195,10 +252,8 @@ public function setHost($pattern, array $defaults = [], array $requirements = [] * Sets a condition on all routes. * * Existing conditions will be overridden. - * - * @param string $condition The condition */ - public function setCondition($condition) + public function setCondition(?string $condition) { foreach ($this->routes as $route) { $route->setCondition($condition); @@ -209,8 +264,6 @@ public function setCondition($condition) * Adds defaults to all routes. * * An existing default value under the same name in a route will be overridden. - * - * @param array $defaults An array of default values */ public function addDefaults(array $defaults) { @@ -225,8 +278,6 @@ public function addDefaults(array $defaults) * Adds requirements to all routes. * * An existing requirement under the same name in a route will be overridden. - * - * @param array $requirements An array of requirements */ public function addRequirements(array $requirements) { @@ -278,7 +329,7 @@ public function setMethods($methods) /** * Returns an array of resources loaded to build this collection. * - * @return ResourceInterface[] An array of resources + * @return ResourceInterface[] */ public function getResources() { @@ -297,4 +348,36 @@ public function addResource(ResourceInterface $resource) $this->resources[$key] = $resource; } } + + /** + * Sets an alias for an existing route. + * + * @param string $name The alias to create + * @param string $alias The route to alias + * + * @throws InvalidArgumentException if the alias is for itself + */ + public function addAlias(string $name, string $alias): Alias + { + if ($name === $alias) { + throw new InvalidArgumentException(sprintf('Route alias "%s" can not reference itself.', $name)); + } + + unset($this->routes[$name], $this->priorities[$name]); + + return $this->aliases[$name] = new Alias($alias); + } + + /** + * @return array + */ + public function getAliases(): array + { + return $this->aliases; + } + + public function getAlias(string $name): ?Alias + { + return $this->aliases[$name] ?? null; + } } diff --git a/symfony/routing/RouteCollectionBuilder.php b/symfony/routing/RouteCollectionBuilder.php index 02740262b..d7eed31eb 100644 --- a/symfony/routing/RouteCollectionBuilder.php +++ b/symfony/routing/RouteCollectionBuilder.php @@ -14,11 +14,16 @@ 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 { @@ -48,15 +53,13 @@ public function __construct(LoaderInterface $loader = null) * * $routes->import('blog.yml', '/blog'); * - * @param mixed $resource - * @param string|null $prefix - * @param string $type + * @param mixed $resource * * @return self * * @throws LoaderLoadException */ - public function import($resource, $prefix = '/', $type = null) + public function import($resource, string $prefix = '/', string $type = null) { /** @var RouteCollection[] $collections */ $collections = $this->load($resource, $type); @@ -87,13 +90,9 @@ public function import($resource, $prefix = '/', $type = null) /** * Adds a route and returns it for future modification. * - * @param string $path The route path - * @param string $controller The route's controller - * @param string|null $name The name to give this route - * * @return Route */ - public function add($path, $controller, $name = null) + public function add(string $path, string $controller, string $name = null) { $route = new Route($path); $route->setDefault('_controller', $controller); @@ -114,10 +113,8 @@ public function createBuilder() /** * Add a RouteCollectionBuilder. - * - * @param string $prefix */ - public function mount($prefix, self $builder) + public function mount(string $prefix, self $builder) { $builder->prefix = trim(trim($prefix), '/'); $this->routes[] = $builder; @@ -126,11 +123,9 @@ public function mount($prefix, self $builder) /** * Adds a Route object to the builder. * - * @param string|null $name - * * @return $this */ - public function addRoute(Route $route, $name = null) + public function addRoute(Route $route, string $name = null) { if (null === $name) { // used as a flag to know which routes will need a name later @@ -145,11 +140,9 @@ public function addRoute(Route $route, $name = null) /** * Sets the host on all embedded routes (unless already set). * - * @param string $pattern - * * @return $this */ - public function setHost($pattern) + public function setHost(?string $pattern) { $this->host = $pattern; @@ -159,11 +152,9 @@ public function setHost($pattern) /** * Sets a condition on all embedded routes (unless already set). * - * @param string $condition - * * @return $this */ - public function setCondition($condition) + public function setCondition(?string $condition) { $this->condition = $condition; @@ -174,12 +165,11 @@ public function setCondition($condition) * Sets a default value that will be added to all embedded routes (unless that * default value is already set). * - * @param string $key - * @param mixed $value + * @param mixed $value * * @return $this */ - public function setDefault($key, $value) + public function setDefault(string $key, $value) { $this->defaults[$key] = $value; @@ -190,12 +180,11 @@ public function setDefault($key, $value) * Sets a requirement that will be added to all embedded routes (unless that * requirement is already set). * - * @param string $key - * @param mixed $regex + * @param mixed $regex * * @return $this */ - public function setRequirement($key, $regex) + public function setRequirement(string $key, $regex) { $this->requirements[$key] = $regex; @@ -206,12 +195,11 @@ public function setRequirement($key, $regex) * Sets an option that will be added to all embedded routes (unless that * option is already set). * - * @param string $key - * @param mixed $value + * @param mixed $value * * @return $this */ - public function setOption($key, $value) + public function setOption(string $key, $value) { $this->options[$key] = $value; diff --git a/symfony/routing/RouteCompiler.php b/symfony/routing/RouteCompiler.php index 6299fa66b..7e78c2931 100644 --- a/symfony/routing/RouteCompiler.php +++ b/symfony/routing/RouteCompiler.php @@ -19,6 +19,9 @@ */ class RouteCompiler implements RouteCompilerInterface { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0 + */ public const REGEX_DELIMITER = '#'; /** @@ -62,7 +65,7 @@ public static function compile(Route $route) } $locale = $route->getDefault('_locale'); - if (null !== $locale && null !== $route->getDefault('_canonical_route') && preg_quote($locale, self::REGEX_DELIMITER) === $route->getRequirement('_locale')) { + if (null !== $locale && null !== $route->getDefault('_canonical_route') && preg_quote($locale) === $route->getRequirement('_locale')) { $requirements = $route->getRequirements(); unset($requirements['_locale']); $route->setRequirements($requirements); @@ -152,7 +155,7 @@ private static function compilePattern(Route $route, string $pattern, bool $isHo if ($isSeparator && $precedingText !== $precedingChar) { $tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))]; - } elseif (!$isSeparator && \strlen($precedingText) > 0) { + } elseif (!$isSeparator && '' !== $precedingText) { $tokens[] = ['text', $precedingText]; } @@ -169,8 +172,8 @@ private static function compilePattern(Route $route, string $pattern, bool $isHo $nextSeparator = self::findNextSeparator($followingPattern, $useUtf8); $regexp = sprintf( '[^%s%s]+', - preg_quote($defaultSeparator, self::REGEX_DELIMITER), - $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : '' + preg_quote($defaultSeparator), + $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator) : '' ); if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) { // When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive @@ -225,7 +228,7 @@ private static function compilePattern(Route $route, string $pattern, bool $isHo for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) { $regexp .= self::computeRegexp($tokens, $i, $firstOptional); } - $regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'sD'.($isHost ? 'i' : ''); + $regexp = '{^'.$regexp.'$}sD'.($isHost ? 'i' : ''); // enable Utf8 matching if really required if ($needsUtf8) { @@ -289,22 +292,20 @@ private static function findNextSeparator(string $pattern, bool $useUtf8): strin * @param array $tokens The route tokens * @param int $index The index of the current token * @param int $firstOptional The index of the first optional token - * - * @return string The regexp pattern for a single token */ private static function computeRegexp(array $tokens, int $index, int $firstOptional): string { $token = $tokens[$index]; if ('text' === $token[0]) { // Text tokens - return preg_quote($token[1], self::REGEX_DELIMITER); + return preg_quote($token[1]); } else { // Variable tokens if (0 === $index && 0 === $firstOptional) { // When the only token is an optional variable token, the separator is required - return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]); + return sprintf('%s(?P<%s>%s)?', preg_quote($token[1]), $token[3], $token[2]); } else { - $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]); + $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1]), $token[3], $token[2]); if ($index >= $firstOptional) { // Enclose each optional token in a subpattern to make it optional. // "?:" means it is non-capturing, i.e. the portion of the subject string that diff --git a/symfony/routing/RouteCompilerInterface.php b/symfony/routing/RouteCompilerInterface.php index ddfa7ca49..9bae33a91 100644 --- a/symfony/routing/RouteCompilerInterface.php +++ b/symfony/routing/RouteCompilerInterface.php @@ -21,7 +21,7 @@ interface RouteCompilerInterface /** * Compiles the current route instance. * - * @return CompiledRoute A CompiledRoute instance + * @return CompiledRoute * * @throws \LogicException If the Route cannot be compiled because the * path or host pattern is invalid diff --git a/symfony/routing/Router.php b/symfony/routing/Router.php index 30c6e5261..25b9456af 100644 --- a/symfony/routing/Router.php +++ b/symfony/routing/Router.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Routing; use Psr\Log\LoggerInterface; -use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher; use Symfony\Component\Config\ConfigCacheFactory; use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; @@ -23,15 +22,11 @@ use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper; use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface; -use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper; -use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Matcher\CompiledUrlMatcher; use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface; -use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; -use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; /** @@ -135,13 +130,9 @@ public function setOptions(array $options) 'cache_dir' => null, 'debug' => false, 'generator_class' => CompiledUrlGenerator::class, - 'generator_base_class' => UrlGenerator::class, // deprecated 'generator_dumper_class' => CompiledUrlGeneratorDumper::class, - 'generator_cache_class' => 'UrlGenerator', // deprecated 'matcher_class' => CompiledUrlMatcher::class, - 'matcher_base_class' => UrlMatcher::class, // deprecated 'matcher_dumper_class' => CompiledUrlMatcherDumper::class, - 'matcher_cache_class' => 'UrlMatcher', // deprecated 'resource_type' => null, 'strict_requirements' => true, ]; @@ -149,7 +140,6 @@ public function setOptions(array $options) // check option names and live merge, if errors are encountered Exception will be thrown $invalid = []; foreach ($options as $key => $value) { - $this->checkDeprecatedOption($key); if (\array_key_exists($key, $this->options)) { $this->options[$key] = $value; } else { @@ -165,39 +155,32 @@ public function setOptions(array $options) /** * Sets an option. * - * @param string $key The key - * @param mixed $value The value + * @param mixed $value The value * * @throws \InvalidArgumentException */ - public function setOption($key, $value) + public function setOption(string $key, $value) { if (!\array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); } - $this->checkDeprecatedOption($key); - $this->options[$key] = $value; } /** * Gets an option value. * - * @param string $key The key - * - * @return mixed The value + * @return mixed * * @throws \InvalidArgumentException */ - public function getOption($key) + public function getOption(string $key) { if (!\array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); } - $this->checkDeprecatedOption($key); - return $this->options[$key]; } @@ -247,7 +230,7 @@ public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFa /** * {@inheritdoc} */ - public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH) { return $this->getGenerator()->generate($name, $parameters, $referenceType); } @@ -255,7 +238,7 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT /** * {@inheritdoc} */ - public function match($pathinfo) + public function match(string $pathinfo) { return $this->getMatcher()->match($pathinfo); } @@ -285,10 +268,9 @@ public function getMatcher() return $this->matcher; } - $compiled = is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true) && (UrlMatcher::class === $this->options['matcher_base_class'] || RedirectableUrlMatcher::class === $this->options['matcher_base_class']) && is_a($this->options['matcher_dumper_class'], CompiledUrlMatcherDumper::class, true); - - if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) { + if (null === $this->options['cache_dir']) { $routes = $this->getRouteCollection(); + $compiled = is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true); if ($compiled) { $routes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes(); } @@ -302,7 +284,7 @@ public function getMatcher() return $this->matcher; } - $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$this->options['matcher_cache_class'].'.php', + $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_matching_routes.php', function (ConfigCacheInterface $cache) { $dumper = $this->getMatcherDumperInstance(); if (method_exists($dumper, 'addExpressionLanguageProvider')) { @@ -311,30 +293,17 @@ function (ConfigCacheInterface $cache) { } } - $options = [ - 'class' => $this->options['matcher_cache_class'], - 'base_class' => $this->options['matcher_base_class'], - ]; - - $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources()); + $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); } ); - if ($compiled) { - return $this->matcher = new $this->options['matcher_class'](self::getCompiledRoutes($cache->getPath()), $this->context); - } - - if (!class_exists($this->options['matcher_cache_class'], false)) { - require_once $cache->getPath(); - } - - return $this->matcher = new $this->options['matcher_cache_class']($this->context); + return $this->matcher = new $this->options['matcher_class'](self::getCompiledRoutes($cache->getPath()), $this->context); } /** * Gets the UrlGenerator instance associated with this Router. * - * @return UrlGeneratorInterface A UrlGeneratorInterface instance + * @return UrlGeneratorInterface */ public function getGenerator() { @@ -342,37 +311,24 @@ public function getGenerator() return $this->generator; } - $compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true) && UrlGenerator::class === $this->options['generator_base_class'] && is_a($this->options['generator_dumper_class'], CompiledUrlGeneratorDumper::class, true); - - if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) { + if (null === $this->options['cache_dir']) { $routes = $this->getRouteCollection(); + $compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true); if ($compiled) { - $routes = (new CompiledUrlGeneratorDumper($routes))->getCompiledRoutes(); + $generatorDumper = new CompiledUrlGeneratorDumper($routes); + $routes = array_merge($generatorDumper->getCompiledRoutes(), $generatorDumper->getCompiledAliases()); } $this->generator = new $this->options['generator_class']($routes, $this->context, $this->logger, $this->defaultLocale); } else { - $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$this->options['generator_cache_class'].'.php', + $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_generating_routes.php', function (ConfigCacheInterface $cache) { $dumper = $this->getGeneratorDumperInstance(); - $options = [ - 'class' => $this->options['generator_cache_class'], - 'base_class' => $this->options['generator_base_class'], - ]; - - $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources()); + $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); } ); - if ($compiled) { - $this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale); - } else { - if (!class_exists($this->options['generator_cache_class'], false)) { - require_once $cache->getPath(); - } - - $this->generator = new $this->options['generator_cache_class']($this->context, $this->logger, $this->defaultLocale); - } + $this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale); } if ($this->generator instanceof ConfigurableRequirementsInterface) { @@ -392,11 +348,6 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac */ protected function getGeneratorDumperInstance() { - // For BC, fallback to PhpGeneratorDumper (which is the old default value) if the old UrlGenerator is used with the new default CompiledUrlGeneratorDumper - if (!is_a($this->options['generator_class'], CompiledUrlGenerator::class, true) && is_a($this->options['generator_dumper_class'], CompiledUrlGeneratorDumper::class, true)) { - return new PhpGeneratorDumper($this->getRouteCollection()); - } - return new $this->options['generator_dumper_class']($this->getRouteCollection()); } @@ -405,11 +356,6 @@ protected function getGeneratorDumperInstance() */ protected function getMatcherDumperInstance() { - // For BC, fallback to PhpMatcherDumper (which is the old default value) if the old UrlMatcher is used with the new default CompiledUrlMatcherDumper - if (!is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true) && is_a($this->options['matcher_dumper_class'], CompiledUrlMatcherDumper::class, true)) { - return new PhpMatcherDumper($this->getRouteCollection()); - } - return new $this->options['matcher_dumper_class']($this->getRouteCollection()); } @@ -426,20 +372,9 @@ private function getConfigCacheFactory(): ConfigCacheFactoryInterface return $this->configCacheFactory; } - private function checkDeprecatedOption(string $key) - { - switch ($key) { - case 'generator_base_class': - case 'generator_cache_class': - case 'matcher_base_class': - case 'matcher_cache_class': - @trigger_error(sprintf('Option "%s" given to router %s is deprecated since Symfony 4.3.', $key, static::class), \E_USER_DEPRECATED); - } - } - 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_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) { self::$cache = null; } diff --git a/symfony/routing/RouterInterface.php b/symfony/routing/RouterInterface.php index 8a3e33dc2..6912f8a15 100644 --- a/symfony/routing/RouterInterface.php +++ b/symfony/routing/RouterInterface.php @@ -29,7 +29,7 @@ interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface * WARNING: This method should never be used at runtime as it is SLOW. * You might use it in a cache warmer though. * - * @return RouteCollection A RouteCollection instance + * @return RouteCollection */ public function getRouteCollection(); } diff --git a/symfony/translation/Catalogue/AbstractOperation.php b/symfony/translation/Catalogue/AbstractOperation.php index 4953563db..98d42e5b6 100644 --- a/symfony/translation/Catalogue/AbstractOperation.php +++ b/symfony/translation/Catalogue/AbstractOperation.php @@ -26,6 +26,10 @@ */ abstract class AbstractOperation implements OperationInterface { + public const OBSOLETE_BATCH = 'obsolete'; + public const NEW_BATCH = 'new'; + public const ALL_BATCH = 'all'; + protected $source; protected $target; protected $result; @@ -79,7 +83,18 @@ public function __construct(MessageCatalogueInterface $source, MessageCatalogueI public function getDomains() { if (null === $this->domains) { - $this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains()))); + $domains = []; + foreach ([$this->source, $this->target] as $catalogue) { + foreach ($catalogue->getDomains() as $domain) { + $domains[$domain] = $domain; + + if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) { + $domains[$domainIcu] = $domainIcu; + } + } + } + + $this->domains = array_values($domains); } return $this->domains; @@ -88,49 +103,49 @@ public function getDomains() /** * {@inheritdoc} */ - public function getMessages($domain) + public function getMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } - if (!isset($this->messages[$domain]['all'])) { + if (!isset($this->messages[$domain][self::ALL_BATCH])) { $this->processDomain($domain); } - return $this->messages[$domain]['all']; + return $this->messages[$domain][self::ALL_BATCH]; } /** * {@inheritdoc} */ - public function getNewMessages($domain) + public function getNewMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } - if (!isset($this->messages[$domain]['new'])) { + if (!isset($this->messages[$domain][self::NEW_BATCH])) { $this->processDomain($domain); } - return $this->messages[$domain]['new']; + return $this->messages[$domain][self::NEW_BATCH]; } /** * {@inheritdoc} */ - public function getObsoleteMessages($domain) + public function getObsoleteMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } - if (!isset($this->messages[$domain]['obsolete'])) { + if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) { $this->processDomain($domain); } - return $this->messages[$domain]['obsolete']; + return $this->messages[$domain][self::OBSOLETE_BATCH]; } /** @@ -147,11 +162,42 @@ public function getResult() return $this->result; } + /** + * @param self::*_BATCH $batch + */ + public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void + { + // If MessageFormatter class does not exists, intl domains are not supported. + if (!class_exists(\MessageFormatter::class)) { + return; + } + + foreach ($this->getDomains() as $domain) { + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + switch ($batch) { + case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break; + case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break; + case self::ALL_BATCH: $messages = $this->getMessages($domain); break; + default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH)); + } + + if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) { + continue; + } + + $result = $this->getResult(); + $allIntlMessages = $result->all($intlDomain); + $currentMessages = array_diff_key($messages, $result->all($domain)); + $result->replace($currentMessages, $domain); + $result->replace($allIntlMessages + $messages, $intlDomain); + } + } + /** * Performs operation on source and target catalogues for the given domain and * stores the results. * * @param string $domain The domain which the operation will be performed for */ - abstract protected function processDomain($domain); + abstract protected function processDomain(string $domain); } diff --git a/symfony/translation/Catalogue/MergeOperation.php b/symfony/translation/Catalogue/MergeOperation.php index d55542c0c..87db2fb03 100644 --- a/symfony/translation/Catalogue/MergeOperation.php +++ b/symfony/translation/Catalogue/MergeOperation.php @@ -27,7 +27,7 @@ class MergeOperation extends AbstractOperation /** * {@inheritdoc} */ - protected function processDomain($domain) + protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], diff --git a/symfony/translation/Catalogue/OperationInterface.php b/symfony/translation/Catalogue/OperationInterface.php index 87d888efb..9ffac88d2 100644 --- a/symfony/translation/Catalogue/OperationInterface.php +++ b/symfony/translation/Catalogue/OperationInterface.php @@ -44,29 +44,23 @@ public function getDomains(); /** * Returns all valid messages ('all') after operation. * - * @param string $domain - * * @return array */ - public function getMessages($domain); + public function getMessages(string $domain); /** * Returns new messages ('new') after operation. * - * @param string $domain - * * @return array */ - public function getNewMessages($domain); + public function getNewMessages(string $domain); /** * Returns obsolete messages ('obsolete') after operation. * - * @param string $domain - * * @return array */ - public function getObsoleteMessages($domain); + public function getObsoleteMessages(string $domain); /** * Returns resulting catalogue ('result'). diff --git a/symfony/translation/Catalogue/TargetOperation.php b/symfony/translation/Catalogue/TargetOperation.php index c8b065512..682b5752f 100644 --- a/symfony/translation/Catalogue/TargetOperation.php +++ b/symfony/translation/Catalogue/TargetOperation.php @@ -28,7 +28,7 @@ class TargetOperation extends AbstractOperation /** * {@inheritdoc} */ - protected function processDomain($domain) + protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], diff --git a/symfony/translation/Command/TranslationPullCommand.php b/symfony/translation/Command/TranslationPullCommand.php new file mode 100644 index 000000000..e2e7c00dc --- /dev/null +++ b/symfony/translation/Command/TranslationPullCommand.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +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\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Catalogue\TargetOperation; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +/** + * @author Mathieu Santostefano + */ +final class TranslationPullCommand extends Command +{ + use TranslationTrait; + + protected static $defaultName = 'translation:pull'; + protected static $defaultDescription = 'Pull translations from a given provider.'; + + private $providerCollection; + private $writer; + private $reader; + private $defaultLocale; + private $transPaths; + private $enabledLocales; + + public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = []) + { + $this->providerCollection = $providerCollection; + $this->writer = $writer; + $this->reader = $reader; + $this->defaultLocale = $defaultLocale; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('provider')) { + $suggestions->suggestValues($this->providerCollection->keys()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains')) { + $provider = $this->providerCollection->get($input->getArgument('provider')); + + if ($provider && method_exists($provider, 'getDomains')) { + $domains = $provider->getDomains(); + $suggestions->suggestValues($domains); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']); + } + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->providerCollection->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'), + new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pulls translations from the given provider. Only +new translations are pulled, existing ones are not overwritten. + +You can overwrite existing translations (and remove the missing ones on local side) by using the --force flag: + + php %command.full_name% --force provider + +Full example: + + php %command.full_name% provider --force --domains=messages --domains=validators --locales=en + +This command pulls all translations associated with the messages and validators domains for the en locale. +Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case. +Local translations for others domains and locales are ignored. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $provider = $this->providerCollection->get($input->getArgument('provider')); + $force = $input->getOption('force'); + $intlIcu = $input->getOption('intl-icu'); + $locales = $input->getOption('locales') ?: $this->enabledLocales; + $domains = $input->getOption('domains'); + $format = $input->getOption('format'); + $xliffVersion = '1.2'; + + if ($intlIcu && !$force) { + $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.'); + } + + switch ($format) { + case 'xlf20': $xliffVersion = '2.0'; + // no break + case 'xlf12': $format = 'xlf'; + } + + $writeOptions = [ + 'path' => end($this->transPaths), + 'xliff_version' => $xliffVersion, + 'default_locale' => $this->defaultLocale, + ]; + + if (!$domains) { + $domains = $provider->getDomains(); + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($force) { + foreach ($providerTranslations->getCatalogues() as $catalogue) { + $operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue); + if ($intlIcu) { + $operation->moveMessagesToIntlDomainsIfPossible(); + } + $this->writer->write($operation->getResult(), $format, $writeOptions); + } + + $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + // Append pulled translations to local ones. + $localTranslations->addBag($providerTranslations->diff($localTranslations)); + + foreach ($localTranslations->getCatalogues() as $catalogue) { + $this->writer->write($catalogue, $format, $writeOptions); + } + + $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } +} diff --git a/symfony/translation/Command/TranslationPushCommand.php b/symfony/translation/Command/TranslationPushCommand.php new file mode 100644 index 000000000..bf6e8c948 --- /dev/null +++ b/symfony/translation/Command/TranslationPushCommand.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\InvalidArgumentException; +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\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Provider\FilteringProvider; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @author Mathieu Santostefano + */ +final class TranslationPushCommand extends Command +{ + use TranslationTrait; + + protected static $defaultName = 'translation:push'; + protected static $defaultDescription = 'Push translations to a given provider.'; + + private $providers; + private $reader; + private $transPaths; + private $enabledLocales; + + public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = []) + { + $this->providers = $providers; + $this->reader = $reader; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('provider')) { + $suggestions->suggestValues($this->providers->keys()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains')) { + $provider = $this->providers->get($input->getArgument('provider')); + + if ($provider && method_exists($provider, 'getDomains')) { + $domains = $provider->getDomains(); + $suggestions->suggestValues($domains); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + } + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->providers->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'), + new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pushes translations to the given provider. Only new +translations are pushed, existing ones are not overwritten. + +You can overwrite existing translations by using the --force flag: + + php %command.full_name% --force provider + +You can delete provider translations which are not present locally by using the --delete-missing flag: + + php %command.full_name% --delete-missing provider + +Full example: + + php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en + +This command pushes all translations associated with the messages and validators domains for the en locale. +Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case. +Provider translations for others domains and locales are ignored. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $provider = $this->providers->get($input->getArgument('provider')); + + if (!$this->enabledLocales) { + throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME))); + } + + $io = new SymfonyStyle($input, $output); + $domains = $input->getOption('domains'); + $locales = $input->getOption('locales'); + $force = $input->getOption('force'); + $deleteMissing = $input->getOption('delete-missing'); + + if (!$domains && $provider instanceof FilteringProvider) { + $domains = $provider->getDomains(); + } + + // Reading local translations must be done after retrieving the domains from the provider + // in order to manage only translations from configured domains + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + if (!$domains) { + $domains = $this->getDomainsFromTranslatorBag($localTranslations); + } + + if (!$deleteMissing && $force) { + $provider->write($localTranslations); + + $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($deleteMissing) { + $provider->delete($providerTranslations->diff($localTranslations)); + + $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + // Read provider translations again, after missing translations deletion, + // to avoid push freshly deleted translations. + $providerTranslations = $provider->read($domains, $locales); + } + + $translationsToWrite = $localTranslations->diff($providerTranslations); + + if ($force) { + $translationsToWrite->addBag($localTranslations->intersect($providerTranslations)); + } + + $provider->write($translationsToWrite); + + $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array + { + $domains = []; + + foreach ($translatorBag->getCatalogues() as $catalogue) { + $domains += $catalogue->getDomains(); + } + + return array_unique($domains); + } +} diff --git a/symfony/translation/Command/TranslationTrait.php b/symfony/translation/Command/TranslationTrait.php new file mode 100644 index 000000000..eafaffd3f --- /dev/null +++ b/symfony/translation/Command/TranslationTrait.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @internal + */ +trait TranslationTrait +{ + private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag + { + $bag = new TranslatorBag(); + + foreach ($locales as $locale) { + $catalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $this->reader->read($path, $catalogue); + } + + if ($domains) { + foreach ($domains as $domain) { + $bag->addCatalogue($this->filterCatalogue($catalogue, $domain)); + } + } else { + $bag->addCatalogue($catalogue); + } + } + + return $bag; + } + + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue + { + $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); + + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + $filteredCatalogue->add($messages, $domain); + } + foreach ($catalogue->getResources() as $resource) { + $filteredCatalogue->addResource($resource); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $domain); + } + } + + return $filteredCatalogue; + } +} diff --git a/symfony/translation/Command/XliffLintCommand.php b/symfony/translation/Command/XliffLintCommand.php index 80d45bf53..fb2b5f31c 100644 --- a/symfony/translation/Command/XliffLintCommand.php +++ b/symfony/translation/Command/XliffLintCommand.php @@ -11,7 +11,10 @@ namespace Symfony\Component\Translation\Command; +use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -31,6 +34,7 @@ class XliffLintCommand extends Command { protected static $defaultName = 'lint:xliff'; + protected static $defaultDescription = 'Lint an XLIFF file and outputs encountered errors'; private $format; private $displayCorrectFiles; @@ -53,9 +57,9 @@ public function __construct(string $name = null, callable $directoryIteratorProv protected function configure() { $this - ->setDescription('Lint an XLIFF file and outputs encountered errors') + ->setDescription(self::$defaultDescription) ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') - ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') ->setHelp(<<%command.name% command lints an XLIFF file and outputs to STDOUT the first encountered syntax error. @@ -82,22 +86,15 @@ protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); - $this->format = $input->getOption('format'); + $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); } - // @deprecated to be removed in 5.0 if (!$filenames) { - if (0 !== ftell(\STDIN)) { - throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); - } - - @trigger_error('Piping content from STDIN to the "lint:xliff" command without passing the dash symbol "-" as argument is deprecated since Symfony 4.4.', \E_USER_DEPRECATED); - - return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); + throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; @@ -166,15 +163,18 @@ private function display(SymfonyStyle $io, array $files) return $this->displayTxt($io, $files); case 'json': return $this->displayJson($io, $files); + case 'github': + return $this->displayTxt($io, $files, true); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)); } } - private function displayTxt(SymfonyStyle $io, array $filesInfo) + private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false) { $countFiles = \count($filesInfo); $erroredFiles = 0; + $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { @@ -182,9 +182,15 @@ private function displayTxt(SymfonyStyle $io, array $filesInfo) } elseif (!$info['valid']) { ++$erroredFiles; $io->text(' ERROR '.($info['file'] ? sprintf(' in %s', $info['file']) : '')); - $io->listing(array_map(function ($error) { + $io->listing(array_map(function ($error) use ($info, $githubReporter) { // general document errors have a '-1' line number - return -1 === $error['line'] ? $error['message'] : sprintf('Line %d, Column %d: %s', $error['line'], $error['column'], $error['message']); + $line = -1 === $error['line'] ? null : $error['line']; + + if ($githubReporter) { + $githubReporter->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null); + } + + return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']); }, $info['messages'])); } } @@ -270,4 +276,11 @@ private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string return null; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['txt', 'json', 'github']); + } + } } diff --git a/symfony/translation/DataCollector/TranslationDataCollector.php b/symfony/translation/DataCollector/TranslationDataCollector.php index e4f0b3a5a..379130a44 100644 --- a/symfony/translation/DataCollector/TranslationDataCollector.php +++ b/symfony/translation/DataCollector/TranslationDataCollector.php @@ -21,7 +21,7 @@ /** * @author Abdellatif Ait boudad * - * @final since Symfony 4.4 + * @final */ class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface { @@ -47,10 +47,8 @@ public function lateCollect() /** * {@inheritdoc} - * - * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response/*, \Throwable $exception = null*/) + public function collect(Request $request, Response $response, \Throwable $exception = null) { $this->data['locale'] = $this->translator->getLocale(); $this->data['fallback_locales'] = $this->translator->getFallbackLocales(); @@ -72,26 +70,17 @@ public function getMessages() return $this->data['messages'] ?? []; } - /** - * @return int - */ - public function getCountMissings() + public function getCountMissings(): int { return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0; } - /** - * @return int - */ - public function getCountFallbacks() + public function getCountFallbacks(): int { return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0; } - /** - * @return int - */ - public function getCountDefines() + public function getCountDefines(): int { return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0; } @@ -102,7 +91,7 @@ public function getLocale() } /** - * @internal since Symfony 4.2 + * @internal */ public function getFallbackLocales() { @@ -112,7 +101,7 @@ public function getFallbackLocales() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'translation'; } diff --git a/symfony/translation/DataCollectorTranslator.php b/symfony/translation/DataCollectorTranslator.php index 68acf1561..ea5a2dd5e 100644 --- a/symfony/translation/DataCollectorTranslator.php +++ b/symfony/translation/DataCollectorTranslator.php @@ -13,36 +13,28 @@ use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface, WarmableInterface +class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface { public const MESSAGE_DEFINED = 0; public const MESSAGE_MISSING = 1; public const MESSAGE_EQUALS_FALLBACK = 2; - /** - * @var TranslatorInterface|TranslatorBagInterface - */ private $translator; - private $messages = []; /** - * @param TranslatorInterface $translator The translator must implement TranslatorBagInterface + * @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator */ - public function __construct($translator) + public function __construct(TranslatorInterface $translator) { - if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { - throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', \get_class($translator))); + throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; @@ -51,9 +43,9 @@ public function __construct($translator) /** * {@inheritdoc} */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { - $trans = $this->translator->trans($id, $parameters, $domain, $locale); + $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->collectMessage($locale, $domain, $id, $trans, $parameters); return $trans; @@ -61,60 +53,54 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul /** * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) + public function setLocale(string $locale) { - if ($this->translator instanceof TranslatorInterface) { - $trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } else { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } - - $this->collectMessage($locale, $domain, $id, $trans, ['%count%' => $number] + $parameters); - - return $trans; + $this->translator->setLocale($locale); } /** * {@inheritdoc} */ - public function setLocale($locale) + public function getLocale() { - $this->translator->setLocale($locale); + return $this->translator->getLocale(); } /** * {@inheritdoc} */ - public function getLocale() + public function getCatalogue(string $locale = null) { - return $this->translator->getLocale(); + return $this->translator->getCatalogue($locale); } /** * {@inheritdoc} */ - public function getCatalogue($locale = null) + public function getCatalogues(): array { - return $this->translator->getCatalogue($locale); + return $this->translator->getCatalogues(); } /** * {@inheritdoc} + * + * @return string[] */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir) { if ($this->translator instanceof WarmableInterface) { - $this->translator->warmUp($cacheDir); + return (array) $this->translator->warmUp($cacheDir); } + + return []; } /** * Gets the fallback locales. * - * @return array The fallback locales + * @return array */ public function getFallbackLocales() { @@ -128,7 +114,7 @@ public function getFallbackLocales() /** * Passes through all unknown calls onto the translator object. */ - public function __call($method, $args) + public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } @@ -141,13 +127,12 @@ public function getCollectedMessages() return $this->messages; } - private function collectMessage(?string $locale, ?string $domain, ?string $id, string $translation, ?array $parameters = []) + private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []) { if (null === $domain) { $domain = 'messages'; } - $id = (string) $id; $catalogue = $this->translator->getCatalogue($locale); $locale = $catalogue->getLocale(); $fallbackLocale = null; diff --git a/symfony/translation/DependencyInjection/TranslationDumperPass.php b/symfony/translation/DependencyInjection/TranslationDumperPass.php index 930f36d72..6d78342bc 100644 --- a/symfony/translation/DependencyInjection/TranslationDumperPass.php +++ b/symfony/translation/DependencyInjection/TranslationDumperPass.php @@ -25,6 +25,10 @@ class TranslationDumperPass implements CompilerPassInterface public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper') { + if (1 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->writerServiceId = $writerServiceId; $this->dumperTag = $dumperTag; } diff --git a/symfony/translation/DependencyInjection/TranslationExtractorPass.php b/symfony/translation/DependencyInjection/TranslationExtractorPass.php index d08b2ba5f..fab6b20ae 100644 --- a/symfony/translation/DependencyInjection/TranslationExtractorPass.php +++ b/symfony/translation/DependencyInjection/TranslationExtractorPass.php @@ -26,6 +26,10 @@ class TranslationExtractorPass implements CompilerPassInterface public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->extractorServiceId = $extractorServiceId; $this->extractorTag = $extractorTag; } diff --git a/symfony/translation/DependencyInjection/TranslatorPass.php b/symfony/translation/DependencyInjection/TranslatorPass.php index ed4a840d8..e6a4b362f 100644 --- a/symfony/translation/DependencyInjection/TranslatorPass.php +++ b/symfony/translation/DependencyInjection/TranslatorPass.php @@ -24,8 +24,12 @@ class TranslatorPass implements CompilerPassInterface private $debugCommandServiceId; private $updateCommandServiceId; - public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update') + public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->translatorServiceId = $translatorServiceId; $this->readerServiceId = $readerServiceId; $this->loaderTag = $loaderTag; @@ -68,7 +72,7 @@ public function process(ContainerBuilder $container) return; } - $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(2)); + $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); if ($container->hasDefinition($this->debugCommandServiceId)) { $definition = $container->getDefinition($this->debugCommandServiceId); $definition->replaceArgument(4, $container->getParameter('twig.default_path')); diff --git a/symfony/translation/DependencyInjection/TranslatorPathsPass.php b/symfony/translation/DependencyInjection/TranslatorPathsPass.php index 37c8c5719..2ee13640c 100644 --- a/symfony/translation/DependencyInjection/TranslatorPathsPass.php +++ b/symfony/translation/DependencyInjection/TranslatorPathsPass.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; /** * @author Yonel Ceruto @@ -27,12 +28,28 @@ class TranslatorPathsPass extends AbstractRecursivePass private $updateCommandServiceId; private $resolverServiceId; private $level = 0; + + /** + * @var array + */ private $paths = []; + + /** + * @var array + */ private $definitions = []; + + /** + * @var array> + */ private $controllers = []; - public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service') + public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract', string $resolverServiceId = 'argument_resolver.service') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->translatorServiceId = $translatorServiceId; $this->debugCommandServiceId = $debugCommandServiceId; $this->updateCommandServiceId = $updateCommandServiceId; @@ -82,7 +99,7 @@ public function process(ContainerBuilder $container) } } - protected function processValue($value, $isRoot = false) + protected function processValue($value, bool $isRoot = false) { if ($value instanceof Reference) { if ((string) $value === $this->translatorServiceId) { @@ -120,28 +137,20 @@ protected function processValue($value, $isRoot = false) private function findControllerArguments(ContainerBuilder $container): array { - if ($container->hasDefinition($this->resolverServiceId)) { - $argument = $container->getDefinition($this->resolverServiceId)->getArgument(0); - if ($argument instanceof Reference) { - $argument = $container->getDefinition($argument); - } - - return $argument->getArgument(0); + if (!$container->has($this->resolverServiceId)) { + return []; } + $resolverDef = $container->findDefinition($this->resolverServiceId); - if ($container->hasDefinition('debug.'.$this->resolverServiceId)) { - $argument = $container->getDefinition('debug.'.$this->resolverServiceId)->getArgument(0); - if ($argument instanceof Reference) { - $argument = $container->getDefinition($argument); - } - $argument = $argument->getArgument(0); - if ($argument instanceof Reference) { - $argument = $container->getDefinition($argument); - } + if (TraceableValueResolver::class === $resolverDef->getClass()) { + $resolverDef = $container->getDefinition($resolverDef->getArgument(0)); + } - return $argument->getArgument(0); + $argument = $resolverDef->getArgument(0); + if ($argument instanceof Reference) { + $argument = $container->getDefinition($argument); } - return []; + return $argument->getArgument(0); } } diff --git a/symfony/translation/Dumper/CsvFileDumper.php b/symfony/translation/Dumper/CsvFileDumper.php index 8f7b032fa..0c8589af8 100644 --- a/symfony/translation/Dumper/CsvFileDumper.php +++ b/symfony/translation/Dumper/CsvFileDumper.php @@ -26,7 +26,7 @@ class CsvFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $handle = fopen('php://memory', 'r+'); @@ -43,11 +43,8 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti /** * Sets the delimiter and escape character for CSV. - * - * @param string $delimiter Delimiter character - * @param string $enclosure Enclosure character */ - public function setCsvControl($delimiter = ';', $enclosure = '"') + public function setCsvControl(string $delimiter = ';', string $enclosure = '"') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; diff --git a/symfony/translation/Dumper/DumperInterface.php b/symfony/translation/Dumper/DumperInterface.php index 445b70189..7cdaef515 100644 --- a/symfony/translation/Dumper/DumperInterface.php +++ b/symfony/translation/Dumper/DumperInterface.php @@ -26,5 +26,5 @@ interface DumperInterface * * @param array $options Options that are used by the dumper */ - public function dump(MessageCatalogue $messages, $options = []); + public function dump(MessageCatalogue $messages, array $options = []); } diff --git a/symfony/translation/Dumper/FileDumper.php b/symfony/translation/Dumper/FileDumper.php index 87d0c69d7..0e1084519 100644 --- a/symfony/translation/Dumper/FileDumper.php +++ b/symfony/translation/Dumper/FileDumper.php @@ -37,31 +37,15 @@ abstract class FileDumper implements DumperInterface * * @param string $relativePathTemplate A template for the relative paths to files */ - public function setRelativePathTemplate($relativePathTemplate) + public function setRelativePathTemplate(string $relativePathTemplate) { $this->relativePathTemplate = $relativePathTemplate; } - /** - * Sets backup flag. - * - * @param bool $backup - * - * @deprecated since Symfony 4.1 - */ - public function setBackup($backup) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), \E_USER_DEPRECATED); - - if (false !== $backup) { - throw new \LogicException('The backup feature is no longer supported.'); - } - } - /** * {@inheritdoc} */ - public function dump(MessageCatalogue $messages, $options = []) + public function dump(MessageCatalogue $messages, array $options = []) { if (!\array_key_exists('path', $options)) { throw new InvalidArgumentException('The file dumper needs a path option.'); @@ -103,16 +87,14 @@ public function dump(MessageCatalogue $messages, $options = []) /** * Transforms a domain of a message catalogue to its string representation. * - * @param string $domain - * - * @return string representation + * @return string */ - abstract public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []); + abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []); /** * Gets the file extension of the dumper. * - * @return string file extension + * @return string */ abstract protected function getExtension(); diff --git a/symfony/translation/Dumper/IcuResFileDumper.php b/symfony/translation/Dumper/IcuResFileDumper.php index 829e0d0c2..12a7a8cfc 100644 --- a/symfony/translation/Dumper/IcuResFileDumper.php +++ b/symfony/translation/Dumper/IcuResFileDumper.php @@ -28,7 +28,7 @@ class IcuResFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $data = $indexes = $resources = ''; @@ -47,7 +47,7 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti $data .= pack('V', \strlen($target)) .mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8') .$this->writePadding($data) - ; + ; } $resOffset = $this->getPosition($data); @@ -56,7 +56,7 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti .$indexes .$this->writePadding($data) .$resources - ; + ; $bundleTop = $this->getPosition($data); diff --git a/symfony/translation/Dumper/IniFileDumper.php b/symfony/translation/Dumper/IniFileDumper.php index 45ff9614b..93c900a4a 100644 --- a/symfony/translation/Dumper/IniFileDumper.php +++ b/symfony/translation/Dumper/IniFileDumper.php @@ -23,7 +23,7 @@ class IniFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $output = ''; diff --git a/symfony/translation/Dumper/JsonFileDumper.php b/symfony/translation/Dumper/JsonFileDumper.php index c6c3163b2..34c0b5694 100644 --- a/symfony/translation/Dumper/JsonFileDumper.php +++ b/symfony/translation/Dumper/JsonFileDumper.php @@ -23,7 +23,7 @@ class JsonFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT; diff --git a/symfony/translation/Dumper/MoFileDumper.php b/symfony/translation/Dumper/MoFileDumper.php index 5a96dacec..f52206ee3 100644 --- a/symfony/translation/Dumper/MoFileDumper.php +++ b/symfony/translation/Dumper/MoFileDumper.php @@ -24,7 +24,7 @@ class MoFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $sources = $targets = $sourceOffsets = $targetOffsets = ''; $offsets = []; @@ -62,7 +62,7 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti .$targetOffsets .$sources .$targets - ; + ; return $output; } diff --git a/symfony/translation/Dumper/PhpFileDumper.php b/symfony/translation/Dumper/PhpFileDumper.php index e77afc2fb..6163b5297 100644 --- a/symfony/translation/Dumper/PhpFileDumper.php +++ b/symfony/translation/Dumper/PhpFileDumper.php @@ -23,7 +23,7 @@ class PhpFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { return "all($domain), true).";\n"; } diff --git a/symfony/translation/Dumper/PoFileDumper.php b/symfony/translation/Dumper/PoFileDumper.php index 1a723f2d0..0d822818c 100644 --- a/symfony/translation/Dumper/PoFileDumper.php +++ b/symfony/translation/Dumper/PoFileDumper.php @@ -23,7 +23,7 @@ class PoFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $output = 'msgid ""'."\n"; $output .= 'msgstr ""'."\n"; diff --git a/symfony/translation/Dumper/QtFileDumper.php b/symfony/translation/Dumper/QtFileDumper.php index 79a64b243..406e9f006 100644 --- a/symfony/translation/Dumper/QtFileDumper.php +++ b/symfony/translation/Dumper/QtFileDumper.php @@ -23,7 +23,7 @@ class QtFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; diff --git a/symfony/translation/Dumper/XliffFileDumper.php b/symfony/translation/Dumper/XliffFileDumper.php index 37e162aa9..f7dbdcddf 100644 --- a/symfony/translation/Dumper/XliffFileDumper.php +++ b/symfony/translation/Dumper/XliffFileDumper.php @@ -24,7 +24,7 @@ class XliffFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $xliffVersion = '1.2'; if (\array_key_exists('xliff_version', $options)) { diff --git a/symfony/translation/Dumper/YamlFileDumper.php b/symfony/translation/Dumper/YamlFileDumper.php index 520ae3340..0b21e8c83 100644 --- a/symfony/translation/Dumper/YamlFileDumper.php +++ b/symfony/translation/Dumper/YamlFileDumper.php @@ -33,7 +33,7 @@ public function __construct(string $extension = 'yml') /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { if (!class_exists(Yaml::class)) { throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.'); diff --git a/symfony/translation/Exception/IncompleteDsnException.php b/symfony/translation/Exception/IncompleteDsnException.php new file mode 100644 index 000000000..cb0ce027e --- /dev/null +++ b/symfony/translation/Exception/IncompleteDsnException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +class IncompleteDsnException extends InvalidArgumentException +{ + public function __construct(string $message, string $dsn = null, \Throwable $previous = null) + { + if ($dsn) { + $message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message; + } + + parent::__construct($message, 0, $previous); + } +} diff --git a/symfony/translation/Exception/MissingRequiredOptionException.php b/symfony/translation/Exception/MissingRequiredOptionException.php new file mode 100644 index 000000000..2b5f80806 --- /dev/null +++ b/symfony/translation/Exception/MissingRequiredOptionException.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\Translation\Exception; + +/** + * @author Oskar Stark + */ +class MissingRequiredOptionException extends IncompleteDsnException +{ + public function __construct(string $option, string $dsn = null, \Throwable $previous = null) + { + $message = sprintf('The option "%s" is required but missing.', $option); + + parent::__construct($message, $dsn, $previous); + } +} diff --git a/symfony/translation/Exception/ProviderException.php b/symfony/translation/Exception/ProviderException.php new file mode 100644 index 000000000..571920da3 --- /dev/null +++ b/symfony/translation/Exception/ProviderException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +class ProviderException extends RuntimeException implements ProviderExceptionInterface +{ + private $response; + private $debug; + + public function __construct(string $message, ResponseInterface $response, int $code = 0, \Exception $previous = null) + { + $this->response = $response; + $this->debug = $response->getInfo('debug') ?? ''; + + parent::__construct($message, $code, $previous); + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } + + public function getDebug(): string + { + return $this->debug; + } +} diff --git a/symfony/translation/Exception/ProviderExceptionInterface.php b/symfony/translation/Exception/ProviderExceptionInterface.php new file mode 100644 index 000000000..922e82726 --- /dev/null +++ b/symfony/translation/Exception/ProviderExceptionInterface.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\Translation\Exception; + +/** + * @author Fabien Potencier + */ +interface ProviderExceptionInterface extends ExceptionInterface +{ + /* + * Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface + */ + public function getDebug(): string; +} diff --git a/symfony/translation/Exception/UnsupportedSchemeException.php b/symfony/translation/Exception/UnsupportedSchemeException.php new file mode 100644 index 000000000..7fbaa8f04 --- /dev/null +++ b/symfony/translation/Exception/UnsupportedSchemeException.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Component\Translation\Bridge; +use Symfony\Component\Translation\Provider\Dsn; + +class UnsupportedSchemeException extends LogicException +{ + private const SCHEME_TO_PACKAGE_MAP = [ + 'crowdin' => [ + 'class' => Bridge\Crowdin\CrowdinProviderFactory::class, + 'package' => 'symfony/crowdin-translation-provider', + ], + 'loco' => [ + 'class' => Bridge\Loco\LocoProviderFactory::class, + 'package' => 'symfony/loco-translation-provider', + ], + 'lokalise' => [ + 'class' => Bridge\Lokalise\LokaliseProviderFactory::class, + 'package' => 'symfony/lokalise-translation-provider', + ], + ]; + + public function __construct(Dsn $dsn, string $name = null, array $supported = []) + { + $provider = $dsn->getScheme(); + if (false !== $pos = strpos($provider, '+')) { + $provider = substr($provider, 0, $pos); + } + $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package'])); + + return; + } + + $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); + if ($name && $supported) { + $message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported)); + } + + parent::__construct($message.'.'); + } +} diff --git a/symfony/translation/Extractor/AbstractFileExtractor.php b/symfony/translation/Extractor/AbstractFileExtractor.php index 618df7324..3bc878348 100644 --- a/symfony/translation/Extractor/AbstractFileExtractor.php +++ b/symfony/translation/Extractor/AbstractFileExtractor.php @@ -49,13 +49,11 @@ private function toSplFileInfo(string $file): \SplFileInfo } /** - * @param string $file - * * @return bool * * @throws InvalidArgumentException */ - protected function isFile($file) + protected function isFile(string $file) { if (!is_file($file)) { throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file)); @@ -65,16 +63,14 @@ protected function isFile($file) } /** - * @param string $file - * * @return bool */ - abstract protected function canBeExtracted($file); + abstract protected function canBeExtracted(string $file); /** * @param string|array $resource Files, a file or a directory * - * @return iterable files to be extracted + * @return iterable */ abstract protected function extractFromDirectory($resource); } diff --git a/symfony/translation/Extractor/ChainExtractor.php b/symfony/translation/Extractor/ChainExtractor.php index 2683f5d24..95dcf157c 100644 --- a/symfony/translation/Extractor/ChainExtractor.php +++ b/symfony/translation/Extractor/ChainExtractor.php @@ -29,10 +29,8 @@ class ChainExtractor implements ExtractorInterface /** * Adds a loader to the translation extractor. - * - * @param string $format The format of the loader */ - public function addExtractor($format, ExtractorInterface $extractor) + public function addExtractor(string $format, ExtractorInterface $extractor) { $this->extractors[$format] = $extractor; } @@ -40,7 +38,7 @@ public function addExtractor($format, ExtractorInterface $extractor) /** * {@inheritdoc} */ - public function setPrefix($prefix) + public function setPrefix(string $prefix) { foreach ($this->extractors as $extractor) { $extractor->setPrefix($prefix); diff --git a/symfony/translation/Extractor/ExtractorInterface.php b/symfony/translation/Extractor/ExtractorInterface.php index 34e6b2d6e..e1db8a9c8 100644 --- a/symfony/translation/Extractor/ExtractorInterface.php +++ b/symfony/translation/Extractor/ExtractorInterface.php @@ -30,8 +30,6 @@ public function extract($resource, MessageCatalogue $catalogue); /** * Sets the prefix that should be used for new found messages. - * - * @param string $prefix The prefix */ - public function setPrefix($prefix); + public function setPrefix(string $prefix); } diff --git a/symfony/translation/Extractor/PhpExtractor.php b/symfony/translation/Extractor/PhpExtractor.php index 32389c677..38c08d548 100644 --- a/symfony/translation/Extractor/PhpExtractor.php +++ b/symfony/translation/Extractor/PhpExtractor.php @@ -27,15 +27,11 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface /** * Prefix for new found message. - * - * @var string */ private $prefix = ''; /** * The sequence that captures translation messages. - * - * @var array */ protected $sequences = [ [ @@ -50,25 +46,83 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface ], [ '->', - 'transChoice', + 'trans', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + '\\', + 'Symfony', + '\\', + 'Component', + '\\', + 'Translation', + '\\', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ - '->', - 'trans', + 'new', + '\Symfony\Component\Translation\TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + '\\', + 'Symfony', + '\\', + 'Component', + '\\', + 'Translation', + '\\', + 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ - '->', - 'transChoice', + 'new', + '\Symfony\Component\Translation\TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 't', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 't', '(', self::MESSAGE_TOKEN, ], @@ -90,7 +144,7 @@ public function extract($resource, MessageCatalogue $catalog) /** * {@inheritdoc} */ - public function setPrefix($prefix) + public function setPrefix(string $prefix) { $this->prefix = $prefix; } @@ -207,17 +261,9 @@ private function getValue(\Iterator $tokenIterator) /** * Extracts trans message from PHP tokens. - * - * @param array $tokens - * @param string $filename */ - protected function parseTokens($tokens, MessageCatalogue $catalog/*, string $filename*/) + protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) { - 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_error(sprintf('The "%s()" method will have a new "string $filename" argument in version 5.0, not defining it is deprecated since Symfony 4.3.', __METHOD__), \E_USER_DEPRECATED); - } - $filename = 2 < \func_num_args() ? func_get_arg(2) : ''; - $tokenIterator = new \ArrayIterator($tokens); for ($key = 0; $key < $tokenIterator->count(); ++$key) { @@ -265,13 +311,11 @@ protected function parseTokens($tokens, MessageCatalogue $catalog/*, string $fil } /** - * @param string $file - * * @return bool * * @throws \InvalidArgumentException */ - protected function canBeExtracted($file) + protected function canBeExtracted(string $file) { return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION); } @@ -281,6 +325,10 @@ protected function canBeExtracted($file) */ protected function extractFromDirectory($directory) { + if (!class_exists(Finder::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class)); + } + $finder = new Finder(); return $finder->files()->name('*.php')->in($directory); diff --git a/symfony/translation/Extractor/PhpStringTokenParser.php b/symfony/translation/Extractor/PhpStringTokenParser.php index f468fe5ea..d114cc738 100644 --- a/symfony/translation/Extractor/PhpStringTokenParser.php +++ b/symfony/translation/Extractor/PhpStringTokenParser.php @@ -65,9 +65,9 @@ class PhpStringTokenParser * * @param string $str String token content * - * @return string The parsed string + * @return string */ - public static function parse($str) + public static function parse(string $str) { $bLength = 0; if ('b' === $str[0]) { @@ -91,9 +91,9 @@ public static function parse($str) * @param string $str String without quotes * @param string|null $quote Quote type * - * @return string String with escape sequences parsed + * @return string */ - public static function parseEscapeSequences($str, $quote) + public static function parseEscapeSequences(string $str, string $quote = null) { if (null !== $quote) { $str = str_replace('\\'.$quote, $quote, $str); @@ -125,9 +125,9 @@ private static function parseCallback(array $matches): string * @param string $startToken Doc string start token content (<< - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation\Formatter; - -/** - * @author Abdellatif Ait boudad - * - * @deprecated since Symfony 4.2, use MessageFormatterInterface::format() with a %count% parameter instead - */ -interface ChoiceMessageFormatterInterface -{ - /** - * Formats a localized message pattern with given arguments. - * - * @param string $message The message (may also be an object that can be cast to string) - * @param int $number The number to use to find the indice of the message - * @param string $locale The message locale - * @param array $parameters An array of parameters for the message - * - * @return string - */ - public function choiceFormat($message, $number, $locale, array $parameters = []); -} diff --git a/symfony/translation/Formatter/MessageFormatter.php b/symfony/translation/Formatter/MessageFormatter.php index 785d6f226..040796483 100644 --- a/symfony/translation/Formatter/MessageFormatter.php +++ b/symfony/translation/Formatter/MessageFormatter.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\IdentityTranslator; -use Symfony\Component\Translation\MessageSelector; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols @@ -22,7 +20,7 @@ class_exists(IntlFormatter::class); /** * @author Abdellatif Ait boudad */ -class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface, ChoiceMessageFormatterInterface +class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface { private $translator; private $intlFormatter; @@ -30,14 +28,8 @@ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterf /** * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ - public function __construct($translator = null, IntlFormatterInterface $intlFormatter = null) + public function __construct(TranslatorInterface $translator = null, IntlFormatterInterface $intlFormatter = null) { - if ($translator instanceof MessageSelector) { - $translator = new IdentityTranslator($translator); - } elseif (null !== $translator && !$translator instanceof TranslatorInterface && !$translator instanceof LegacyTranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } - $this->translator = $translator ?? new IdentityTranslator(); $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); } @@ -45,7 +37,7 @@ public function __construct($translator = null, IntlFormatterInterface $intlForm /** * {@inheritdoc} */ - public function format($message, $locale, array $parameters = []) + public function format(string $message, string $locale, array $parameters = []) { if ($this->translator instanceof TranslatorInterface) { return $this->translator->trans($message, $parameters, null, $locale); @@ -61,22 +53,4 @@ public function formatIntl(string $message, string $locale, array $parameters = { return $this->intlFormatter->formatIntl($message, $locale, $parameters); } - - /** - * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use format() with a %count% parameter instead - */ - public function choiceFormat($message, $number, $locale, array $parameters = []) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the format() one instead with a %%count%% parameter.', __METHOD__), \E_USER_DEPRECATED); - - $parameters = ['%count%' => $number] + $parameters; - - if ($this->translator instanceof TranslatorInterface) { - return $this->format($message, $locale, $parameters); - } - - return $this->format($this->translator->transChoice($message, $number, [], null, $locale), $locale, $parameters); - } } diff --git a/symfony/translation/Formatter/MessageFormatterInterface.php b/symfony/translation/Formatter/MessageFormatterInterface.php index 370c05586..b85dbfd11 100644 --- a/symfony/translation/Formatter/MessageFormatterInterface.php +++ b/symfony/translation/Formatter/MessageFormatterInterface.php @@ -26,5 +26,5 @@ interface MessageFormatterInterface * * @return string */ - public function format($message, $locale, array $parameters = []); + public function format(string $message, string $locale, array $parameters = []); } diff --git a/symfony/translation/IdentityTranslator.php b/symfony/translation/IdentityTranslator.php index 864339615..46875edf2 100644 --- a/symfony/translation/IdentityTranslator.php +++ b/symfony/translation/IdentityTranslator.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Translation; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; @@ -20,58 +20,7 @@ * * @author Fabien Potencier */ -class IdentityTranslator implements LegacyTranslatorInterface, TranslatorInterface +class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface { - use TranslatorTrait { - trans as private doTrans; - setLocale as private doSetLocale; - } - - private $selector; - - public function __construct(MessageSelector $selector = null) - { - $this->selector = $selector; - - if (__CLASS__ !== static::class) { - @trigger_error(sprintf('Calling "%s()" is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); - } - } - - /** - * {@inheritdoc} - */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) - { - return $this->doTrans($id, $parameters, $domain, $locale); - } - - /** - * {@inheritdoc} - */ - public function setLocale($locale) - { - $this->doSetLocale($locale); - } - - /** - * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter - */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if ($this->selector) { - return strtr($this->selector->choose((string) $id, $number, $locale ?: $this->getLocale()), $parameters); - } - - return $this->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } - - private function getPluralizationRule(float $number, string $locale): int - { - return PluralizationRules::get($number, $locale, false); - } + use TranslatorTrait; } diff --git a/symfony/translation/Interval.php b/symfony/translation/Interval.php deleted file mode 100644 index bb6b164e8..000000000 --- a/symfony/translation/Interval.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', Interval::class), \E_USER_DEPRECATED); - -use Symfony\Component\Translation\Exception\InvalidArgumentException; - -/** - * Tests if a given number belongs to a given math interval. - * - * An interval can represent a finite set of numbers: - * - * {1,2,3,4} - * - * An interval can represent numbers between two numbers: - * - * [1, +Inf] - * ]-1,2[ - * - * The left delimiter can be [ (inclusive) or ] (exclusive). - * The right delimiter can be [ (exclusive) or ] (inclusive). - * Beside numbers, you can use -Inf and +Inf for the infinite. - * - * @author Fabien Potencier - * - * @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation - * @deprecated since Symfony 4.2, use IdentityTranslator instead - */ -class Interval -{ - /** - * Tests if the given number is in the math interval. - * - * @param int $number A number - * @param string $interval An interval - * - * @return bool - * - * @throws InvalidArgumentException - */ - public static function test($number, $interval) - { - $interval = trim($interval); - - if (!preg_match('/^'.self::getIntervalRegexp().'$/x', $interval, $matches)) { - throw new InvalidArgumentException(sprintf('"%s" is not a valid interval.', $interval)); - } - - if ($matches[1]) { - foreach (explode(',', $matches[2]) as $n) { - if ($number == $n) { - return true; - } - } - } else { - $leftNumber = self::convertNumber($matches['left']); - $rightNumber = self::convertNumber($matches['right']); - - return - ('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) - && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) - ; - } - - return false; - } - - /** - * Returns a Regexp that matches valid intervals. - * - * @return string A Regexp (without the delimiters) - */ - public static function getIntervalRegexp() - { - return <<[\[\]]) - \s* - (?P-Inf|\-?\d+(\.\d+)?) - \s*,\s* - (?P\+?Inf|\-?\d+(\.\d+)?) - \s* - (?P[\[\]]) -EOF; - } - - private static function convertNumber(string $number): float - { - if ('-Inf' === $number) { - return log(0); - } elseif ('+Inf' === $number || 'Inf' === $number) { - return -log(0); - } - - return (float) $number; - } -} diff --git a/symfony/translation/LICENSE b/symfony/translation/LICENSE index 88bf75bb4..0138f8f07 100644 --- a/symfony/translation/LICENSE +++ b/symfony/translation/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2022 Fabien Potencier +Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/symfony/translation/Loader/ArrayLoader.php b/symfony/translation/Loader/ArrayLoader.php index 2e9a4285e..feed0de4b 100644 --- a/symfony/translation/Loader/ArrayLoader.php +++ b/symfony/translation/Loader/ArrayLoader.php @@ -23,7 +23,7 @@ class ArrayLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { $resource = $this->flatten($resource); $catalogue = new MessageCatalogue($locale); @@ -46,9 +46,11 @@ private function flatten(array $messages): array foreach ($messages as $key => $value) { if (\is_array($value)) { foreach ($this->flatten($value) as $k => $v) { - $result[$key.'.'.$k] = $v; + if (null !== $v) { + $result[$key.'.'.$k] = $v; + } } - } else { + } elseif (null !== $value) { $result[$key] = $value; } } diff --git a/symfony/translation/Loader/CsvFileLoader.php b/symfony/translation/Loader/CsvFileLoader.php index db17bd563..8d5d4db9a 100644 --- a/symfony/translation/Loader/CsvFileLoader.php +++ b/symfony/translation/Loader/CsvFileLoader.php @@ -27,7 +27,7 @@ class CsvFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $messages = []; @@ -55,12 +55,8 @@ protected function loadResource($resource) /** * Sets the delimiter, enclosure, and escape character for CSV. - * - * @param string $delimiter Delimiter character - * @param string $enclosure Enclosure character - * @param string $escape Escape character */ - public function setCsvControl($delimiter = ';', $enclosure = '"', $escape = '\\') + public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; diff --git a/symfony/translation/Loader/FileLoader.php b/symfony/translation/Loader/FileLoader.php index 42c687d22..4725ea6d6 100644 --- a/symfony/translation/Loader/FileLoader.php +++ b/symfony/translation/Loader/FileLoader.php @@ -23,7 +23,7 @@ abstract class FileLoader extends ArrayLoader /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); @@ -55,11 +55,9 @@ public function load($resource, $locale, $domain = 'messages') } /** - * @param string $resource - * * @return array * * @throws InvalidResourceException if stream content has an invalid format */ - abstract protected function loadResource($resource); + abstract protected function loadResource(string $resource); } diff --git a/symfony/translation/Loader/IcuDatFileLoader.php b/symfony/translation/Loader/IcuDatFileLoader.php index dfdabc9d4..2a1aecc60 100644 --- a/symfony/translation/Loader/IcuDatFileLoader.php +++ b/symfony/translation/Loader/IcuDatFileLoader.php @@ -26,7 +26,7 @@ class IcuDatFileLoader extends IcuResFileLoader /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource.'.dat')) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); diff --git a/symfony/translation/Loader/IcuResFileLoader.php b/symfony/translation/Loader/IcuResFileLoader.php index 126556fee..6b7834e67 100644 --- a/symfony/translation/Loader/IcuResFileLoader.php +++ b/symfony/translation/Loader/IcuResFileLoader.php @@ -26,7 +26,7 @@ class IcuResFileLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); @@ -71,11 +71,11 @@ public function load($resource, $locale, $domain = 'messages') * * @param \ResourceBundle $rb The ResourceBundle that will be flattened * @param array $messages Used internally for recursive calls - * @param string $path Current path being parsed, used internally for recursive calls + * @param string|null $path Current path being parsed, used internally for recursive calls * - * @return array the flattened ResourceBundle + * @return array */ - protected function flatten(\ResourceBundle $rb, array &$messages = [], $path = null) + protected function flatten(\ResourceBundle $rb, array &$messages = [], string $path = null) { foreach ($rb as $key => $value) { $nodePath = $path ? $path.'.'.$key : $key; diff --git a/symfony/translation/Loader/IniFileLoader.php b/symfony/translation/Loader/IniFileLoader.php index 11d9b272e..7398f7778 100644 --- a/symfony/translation/Loader/IniFileLoader.php +++ b/symfony/translation/Loader/IniFileLoader.php @@ -21,7 +21,7 @@ class IniFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { return parse_ini_file($resource, true); } diff --git a/symfony/translation/Loader/JsonFileLoader.php b/symfony/translation/Loader/JsonFileLoader.php index 8a8996b0d..5aefba072 100644 --- a/symfony/translation/Loader/JsonFileLoader.php +++ b/symfony/translation/Loader/JsonFileLoader.php @@ -23,7 +23,7 @@ class JsonFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $messages = []; if ($data = file_get_contents($resource)) { diff --git a/symfony/translation/Loader/LoaderInterface.php b/symfony/translation/Loader/LoaderInterface.php index 1785402d9..96b0c0d85 100644 --- a/symfony/translation/Loader/LoaderInterface.php +++ b/symfony/translation/Loader/LoaderInterface.php @@ -29,10 +29,10 @@ interface LoaderInterface * @param string $locale A locale * @param string $domain The domain * - * @return MessageCatalogue A MessageCatalogue instance + * @return MessageCatalogue * * @throws NotFoundResourceException when the resource cannot be found * @throws InvalidResourceException when the resource cannot be loaded */ - public function load($resource, $locale, $domain = 'messages'); + public function load($resource, string $locale, string $domain = 'messages'); } diff --git a/symfony/translation/Loader/MoFileLoader.php b/symfony/translation/Loader/MoFileLoader.php index ce8611cdf..7fa2f2dcb 100644 --- a/symfony/translation/Loader/MoFileLoader.php +++ b/symfony/translation/Loader/MoFileLoader.php @@ -41,7 +41,7 @@ class MoFileLoader extends FileLoader * * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $stream = fopen($resource, 'r'); diff --git a/symfony/translation/Loader/PhpFileLoader.php b/symfony/translation/Loader/PhpFileLoader.php index c361d5293..40f64db27 100644 --- a/symfony/translation/Loader/PhpFileLoader.php +++ b/symfony/translation/Loader/PhpFileLoader.php @@ -23,9 +23,9 @@ class PhpFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { - 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_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) { self::$cache = null; } diff --git a/symfony/translation/Loader/PoFileLoader.php b/symfony/translation/Loader/PoFileLoader.php index 5e460fbfb..ee143e203 100644 --- a/symfony/translation/Loader/PoFileLoader.php +++ b/symfony/translation/Loader/PoFileLoader.php @@ -60,7 +60,7 @@ class PoFileLoader extends FileLoader * * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $stream = fopen($resource, 'r'); diff --git a/symfony/translation/Loader/QtFileLoader.php b/symfony/translation/Loader/QtFileLoader.php index 29567789e..9cf2fe976 100644 --- a/symfony/translation/Loader/QtFileLoader.php +++ b/symfony/translation/Loader/QtFileLoader.php @@ -15,6 +15,7 @@ use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** @@ -27,8 +28,12 @@ class QtFileLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the QT format requires the Symfony Config component.'); + } + if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } diff --git a/symfony/translation/Loader/XliffFileLoader.php b/symfony/translation/Loader/XliffFileLoader.php index 35e2d9c10..5c9794a54 100644 --- a/symfony/translation/Loader/XliffFileLoader.php +++ b/symfony/translation/Loader/XliffFileLoader.php @@ -12,9 +12,12 @@ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\Exception\InvalidXmlException; +use Symfony\Component\Config\Util\Exception\XmlParsingException; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\XliffUtils; @@ -28,38 +31,53 @@ class XliffFileLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { - if (!stream_is_local($resource)) { - throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); } - if (!file_exists($resource)) { - throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + if (!$this->isXmlString($resource)) { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + if (!is_file($resource)) { + throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource)); + } + } + + try { + if ($this->isXmlString($resource)) { + $dom = XmlUtils::parse($resource); + } else { + $dom = XmlUtils::loadFile($resource); + } + } catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); + } + + if ($errors = XliffUtils::validateSchema($dom)) { + throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); } $catalogue = new MessageCatalogue($locale); - $this->extract($resource, $catalogue, $domain); + $this->extract($dom, $catalogue, $domain); - if (class_exists(FileResource::class)) { + if (is_file($resource) && class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } - private function extract($resource, MessageCatalogue $catalogue, string $domain) + private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { - try { - $dom = XmlUtils::loadFile($resource); - } catch (\InvalidArgumentException $e) { - throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); - } - $xliffVersion = XliffUtils::getVersionNumber($dom); - if ($errors = XliffUtils::validateSchema($dom)) { - throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); - } if ('1.2' === $xliffVersion) { $this->extractXliff1($dom, $catalogue, $domain); @@ -135,11 +153,12 @@ private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, s foreach ($xml->xpath('//xliff:unit') as $unit) { foreach ($unit->segment as $segment) { - $source = $segment->source; + $attributes = $unit->attributes(); + $source = $attributes['name'] ?? $segment->source; // If the xlf file has another encoding specified, try to convert it because // simple_xml will always return utf-8 encoded values - $target = $this->utf8ToCharset((string) ($segment->target ?? $source), $encoding); + $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); $catalogue->set((string) $source, $target, $domain); @@ -205,4 +224,9 @@ private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, strin return $notes; } + + private function isXmlString(string $resource): bool + { + return 0 === strpos($resource, 'yamlParser) { if (!class_exists(\Symfony\Component\Yaml\Parser::class)) { diff --git a/symfony/translation/LoggingTranslator.php b/symfony/translation/LoggingTranslator.php index d7b6fc4b3..6ccd48289 100644 --- a/symfony/translation/LoggingTranslator.php +++ b/symfony/translation/LoggingTranslator.php @@ -13,32 +13,24 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterface, TranslatorBagInterface +class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { - /** - * @var TranslatorInterface|TranslatorBagInterface - */ private $translator; - private $logger; /** - * @param TranslatorInterface $translator The translator must implement TranslatorBagInterface + * @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator The translator must implement TranslatorBagInterface */ - public function __construct($translator, LoggerInterface $logger) + public function __construct(TranslatorInterface $translator, LoggerInterface $logger) { - if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { - throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', \get_class($translator))); + throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; @@ -48,29 +40,9 @@ public function __construct($translator, LoggerInterface $logger) /** * {@inheritdoc} */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) - { - $trans = $this->translator->trans($id, $parameters, $domain, $locale); - $this->log($id, $domain, $locale); - - return $trans; - } - - /** - * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter - */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) + public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if ($this->translator instanceof TranslatorInterface) { - $trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } else { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } - + $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->log($id, $domain, $locale); return $trans; @@ -79,7 +51,7 @@ public function transChoice($id, $number, array $parameters = [], $domain = null /** * {@inheritdoc} */ - public function setLocale($locale) + public function setLocale(string $locale) { $prev = $this->translator->getLocale(); $this->translator->setLocale($locale); @@ -101,15 +73,23 @@ public function getLocale() /** * {@inheritdoc} */ - public function getCatalogue($locale = null) + public function getCatalogue(string $locale = null) { return $this->translator->getCatalogue($locale); } + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return $this->translator->getCatalogues(); + } + /** * Gets the fallback locales. * - * @return array The fallback locales + * @return array */ public function getFallbackLocales() { @@ -123,7 +103,7 @@ public function getFallbackLocales() /** * Passes through all unknown calls onto the translator object. */ - public function __call($method, $args) + public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } @@ -131,13 +111,12 @@ public function __call($method, $args) /** * Logs for missing translations. */ - private function log(?string $id, ?string $domain, ?string $locale) + private function log(string $id, ?string $domain, ?string $locale) { if (null === $domain) { $domain = 'messages'; } - $id = (string) $id; $catalogue = $this->translator->getCatalogue($locale); if ($catalogue->defines($id, $domain)) { return; diff --git a/symfony/translation/MessageCatalogue.php b/symfony/translation/MessageCatalogue.php index 6e6b9fe0e..9da3b7f00 100644 --- a/symfony/translation/MessageCatalogue.php +++ b/symfony/translation/MessageCatalogue.php @@ -29,12 +29,8 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf /** * @param array $messages An array of messages classified by domain */ - public function __construct(?string $locale, array $messages = []) + public function __construct(string $locale, array $messages = []) { - if (null === $locale) { - @trigger_error(sprintf('Passing "null" to the first argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED); - } - $this->locale = $locale; $this->messages = $messages; } @@ -67,7 +63,7 @@ public function getDomains() /** * {@inheritdoc} */ - public function all($domain = null) + public function all(string $domain = null) { if (null !== $domain) { // skip messages merge if intl-icu requested explicitly @@ -95,7 +91,7 @@ public function all($domain = null) /** * {@inheritdoc} */ - public function set($id, $translation, $domain = 'messages') + public function set(string $id, string $translation, string $domain = 'messages') { $this->add([$id => $translation], $domain); } @@ -103,7 +99,7 @@ public function set($id, $translation, $domain = 'messages') /** * {@inheritdoc} */ - public function has($id, $domain = 'messages') + public function has(string $id, string $domain = 'messages') { if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return true; @@ -119,7 +115,7 @@ public function has($id, $domain = 'messages') /** * {@inheritdoc} */ - public function defines($id, $domain = 'messages') + public function defines(string $id, string $domain = 'messages') { return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]); } @@ -127,7 +123,7 @@ public function defines($id, $domain = 'messages') /** * {@inheritdoc} */ - public function get($id, $domain = 'messages') + public function get(string $id, string $domain = 'messages') { if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]; @@ -147,7 +143,7 @@ public function get($id, $domain = 'messages') /** * {@inheritdoc} */ - public function replace($messages, $domain = 'messages') + public function replace(array $messages, string $domain = 'messages') { unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]); @@ -157,21 +153,16 @@ public function replace($messages, $domain = 'messages') /** * {@inheritdoc} */ - public function add($messages, $domain = 'messages') + public function add(array $messages, string $domain = 'messages') { - if (!isset($this->messages[$domain])) { - $this->messages[$domain] = []; - } - $intlDomain = $domain; - if (!str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { - $intlDomain .= self::INTL_DOMAIN_SUFFIX; - } + $altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX; foreach ($messages as $id => $message) { - if (isset($this->messages[$intlDomain]) && \array_key_exists($id, $this->messages[$intlDomain])) { - $this->messages[$intlDomain][$id] = $message; - } else { - $this->messages[$domain][$id] = $message; - } + unset($this->messages[$altDomain][$id]); + $this->messages[$domain][$id] = $message; + } + + if ([] === ($this->messages[$altDomain] ?? null)) { + unset($this->messages[$altDomain]); } } @@ -261,7 +252,7 @@ public function addResource(ResourceInterface $resource) /** * {@inheritdoc} */ - public function getMetadata($key = '', $domain = 'messages') + public function getMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { return $this->metadata; @@ -283,7 +274,7 @@ public function getMetadata($key = '', $domain = 'messages') /** * {@inheritdoc} */ - public function setMetadata($key, $value, $domain = 'messages') + public function setMetadata(string $key, $value, string $domain = 'messages') { $this->metadata[$domain][$key] = $value; } @@ -291,7 +282,7 @@ public function setMetadata($key, $value, $domain = 'messages') /** * {@inheritdoc} */ - public function deleteMetadata($key = '', $domain = 'messages') + public function deleteMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { $this->metadata = []; diff --git a/symfony/translation/MessageCatalogueInterface.php b/symfony/translation/MessageCatalogueInterface.php index 853ce7031..965bf008f 100644 --- a/symfony/translation/MessageCatalogueInterface.php +++ b/symfony/translation/MessageCatalogueInterface.php @@ -25,14 +25,14 @@ interface MessageCatalogueInterface /** * Gets the catalogue locale. * - * @return string The locale + * @return string */ public function getLocale(); /** * Gets the domains. * - * @return array An array of domains + * @return array */ public function getDomains(); @@ -41,11 +41,11 @@ public function getDomains(); * * If $domain is null, it returns all messages. * - * @param string $domain The domain name + * @param string|null $domain The domain name * - * @return array An array of messages + * @return array */ - public function all($domain = null); + public function all(string $domain = null); /** * Sets a message translation. @@ -54,7 +54,7 @@ public function all($domain = null); * @param string $translation The messages translation * @param string $domain The domain name */ - public function set($id, $translation, $domain = 'messages'); + public function set(string $id, string $translation, string $domain = 'messages'); /** * Checks if a message has a translation. @@ -62,9 +62,9 @@ public function set($id, $translation, $domain = 'messages'); * @param string $id The message id * @param string $domain The domain name * - * @return bool true if the message has a translation, false otherwise + * @return bool */ - public function has($id, $domain = 'messages'); + public function has(string $id, string $domain = 'messages'); /** * Checks if a message has a translation (it does not take into account the fallback mechanism). @@ -72,9 +72,9 @@ public function has($id, $domain = 'messages'); * @param string $id The message id * @param string $domain The domain name * - * @return bool true if the message has a translation, false otherwise + * @return bool */ - public function defines($id, $domain = 'messages'); + public function defines(string $id, string $domain = 'messages'); /** * Gets a message translation. @@ -82,9 +82,9 @@ public function defines($id, $domain = 'messages'); * @param string $id The message id * @param string $domain The domain name * - * @return string The message translation + * @return string */ - public function get($id, $domain = 'messages'); + public function get(string $id, string $domain = 'messages'); /** * Sets translations for a given domain. @@ -92,7 +92,7 @@ public function get($id, $domain = 'messages'); * @param array $messages An array of translations * @param string $domain The domain name */ - public function replace($messages, $domain = 'messages'); + public function replace(array $messages, string $domain = 'messages'); /** * Adds translations for a given domain. @@ -100,7 +100,7 @@ public function replace($messages, $domain = 'messages'); * @param array $messages An array of translations * @param string $domain The domain name */ - public function add($messages, $domain = 'messages'); + public function add(array $messages, string $domain = 'messages'); /** * Merges translations from the given Catalogue into the current one. @@ -120,14 +120,14 @@ public function addFallbackCatalogue(self $catalogue); /** * Gets the fallback catalogue. * - * @return self|null A MessageCatalogueInterface instance or null when no fallback has been set + * @return self|null */ public function getFallbackCatalogue(); /** * Returns an array of resources loaded to build this collection. * - * @return ResourceInterface[] An array of resources + * @return ResourceInterface[] */ public function getResources(); diff --git a/symfony/translation/MessageSelector.php b/symfony/translation/MessageSelector.php deleted file mode 100644 index 38de357a5..000000000 --- a/symfony/translation/MessageSelector.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', MessageSelector::class), \E_USER_DEPRECATED); - -use Symfony\Component\Translation\Exception\InvalidArgumentException; - -/** - * MessageSelector. - * - * @author Fabien Potencier - * @author Bernhard Schussek - * - * @deprecated since Symfony 4.2, use IdentityTranslator instead. - */ -class MessageSelector -{ - /** - * Given a message with different plural translations separated by a - * pipe (|), this method returns the correct portion of the message based - * on the given number, locale and the pluralization rules in the message - * itself. - * - * The message supports two different types of pluralization rules: - * - * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples - * indexed: There is one apple|There are %count% apples - * - * The indexed solution can also contain labels (e.g. one: There is one apple). - * This is purely for making the translations more clear - it does not - * affect the functionality. - * - * The two methods can also be mixed: - * {0} There are no apples|one: There is one apple|more: There are %count% apples - * - * @param string $message The message being translated - * @param int|float $number The number of items represented for the message - * @param string $locale The locale to use for choosing - * - * @return string - * - * @throws InvalidArgumentException - */ - public function choose($message, $number, $locale) - { - $parts = []; - if (preg_match('/^\|++$/', $message)) { - $parts = explode('|', $message); - } elseif (preg_match_all('/(?:\|\||[^\|])++/', $message, $matches)) { - $parts = $matches[0]; - } - - $explicitRules = []; - $standardRules = []; - foreach ($parts as $part) { - $part = trim(str_replace('||', '|', $part)); - - if (preg_match('/^(?P'.Interval::getIntervalRegexp().')\s*(?P.*?)$/xs', $part, $matches)) { - $explicitRules[$matches['interval']] = $matches['message']; - } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { - $standardRules[] = $matches[1]; - } else { - $standardRules[] = $part; - } - } - - // try to match an explicit rule, then fallback to the standard ones - foreach ($explicitRules as $interval => $m) { - if (Interval::test($number, $interval)) { - return $m; - } - } - - $position = PluralizationRules::get($number, $locale); - - if (!isset($standardRules[$position])) { - // when there's exactly one rule given, and that rule is a standard - // rule, use this rule - if (1 === \count($parts) && isset($standardRules[0])) { - return $standardRules[0]; - } - - throw new InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale, $number)); - } - - return $standardRules[$position]; - } -} diff --git a/symfony/translation/MetadataAwareInterface.php b/symfony/translation/MetadataAwareInterface.php index e93c6fbc7..2216eed94 100644 --- a/symfony/translation/MetadataAwareInterface.php +++ b/symfony/translation/MetadataAwareInterface.php @@ -25,30 +25,22 @@ interface MetadataAwareInterface * domain and then by key. Passing an empty key will return an array with all * metadata for the given domain. * - * @param string $key The key - * @param string $domain The domain name - * * @return mixed The value that was set or an array with the domains/keys or null */ - public function getMetadata($key = '', $domain = 'messages'); + public function getMetadata(string $key = '', string $domain = 'messages'); /** * Adds metadata to a message domain. * - * @param string $key The key - * @param mixed $value The value - * @param string $domain The domain name + * @param mixed $value */ - public function setMetadata($key, $value, $domain = 'messages'); + public function setMetadata(string $key, $value, string $domain = 'messages'); /** * Deletes metadata for the given key and domain. * * Passing an empty domain will delete all metadata. Passing an empty key will * delete all metadata for the given domain. - * - * @param string $key The key - * @param string $domain The domain name */ - public function deleteMetadata($key = '', $domain = 'messages'); + public function deleteMetadata(string $key = '', string $domain = 'messages'); } diff --git a/symfony/translation/PluralizationRules.php b/symfony/translation/PluralizationRules.php deleted file mode 100644 index e69ceabc1..000000000 --- a/symfony/translation/PluralizationRules.php +++ /dev/null @@ -1,221 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -/** - * Returns the plural rules for a given locale. - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use IdentityTranslator instead - */ -class PluralizationRules -{ - private static $rules = []; - - /** - * Returns the plural position to use for the given locale and number. - * - * @param float $number The number - * @param string $locale The locale - * - * @return int The plural position - */ - public static function get($number, $locale/*, bool $triggerDeprecation = true*/) - { - $number = abs($number); - - if (3 > \func_num_args() || func_get_arg(2)) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), \E_USER_DEPRECATED); - } - - if ('pt_BR' === $locale) { - // temporary set a locale for brazilian - $locale = 'xbr'; - } - - if ('en_US_POSIX' !== $locale && \strlen($locale) > 3) { - $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); - } - - if (isset(self::$rules[$locale])) { - $return = self::$rules[$locale]($number); - - if (!\is_int($return) || $return < 0) { - return 0; - } - - return $return; - } - - /* - * The plural rules are derived from code of the Zend Framework (2010-09-25), - * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). - * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) - */ - switch ($locale) { - case 'az': - case 'bo': - case 'dz': - case 'id': - case 'ja': - case 'jv': - case 'ka': - case 'km': - case 'kn': - case 'ko': - case 'ms': - case 'th': - case 'tr': - case 'vi': - case 'zh': - return 0; - - case 'af': - case 'bn': - case 'bg': - case 'ca': - case 'da': - case 'de': - case 'el': - case 'en': - case 'en_US_POSIX': - case 'eo': - case 'es': - case 'et': - case 'eu': - case 'fa': - case 'fi': - case 'fo': - case 'fur': - case 'fy': - case 'gl': - case 'gu': - case 'ha': - case 'he': - case 'hu': - case 'is': - case 'it': - case 'ku': - case 'lb': - case 'ml': - case 'mn': - case 'mr': - case 'nah': - case 'nb': - case 'ne': - case 'nl': - case 'nn': - case 'no': - case 'oc': - case 'om': - case 'or': - case 'pa': - case 'pap': - case 'ps': - case 'pt': - case 'so': - case 'sq': - case 'sv': - case 'sw': - case 'ta': - case 'te': - case 'tk': - case 'ur': - case 'zu': - return (1 == $number) ? 0 : 1; - - case 'am': - case 'bh': - case 'fil': - case 'fr': - case 'gun': - case 'hi': - case 'hy': - case 'ln': - case 'mg': - case 'nso': - case 'xbr': - case 'ti': - case 'wa': - return ($number < 2) ? 0 : 1; - - case 'be': - case 'bs': - case 'hr': - case 'ru': - case 'sh': - case 'sr': - case 'uk': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'cs': - case 'sk': - return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); - - case 'ga': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); - - case 'lt': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'sl': - return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); - - case 'mk': - return (1 == $number % 10) ? 0 : 1; - - case 'mt': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); - - case 'lv': - return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); - - case 'pl': - return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); - - case 'cy': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); - - case 'ro': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); - - case 'ar': - return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); - - default: - return 0; - } - } - - /** - * Overrides the default plural rule for a given locale. - * - * @param callable $rule A PHP callable - * @param string $locale The locale - */ - public static function set(callable $rule, $locale) - { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), \E_USER_DEPRECATED); - - if ('pt_BR' === $locale) { - // temporary set a locale for brazilian - $locale = 'xbr'; - } - - if (\strlen($locale) > 3) { - $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); - } - - self::$rules[$locale] = $rule; - } -} diff --git a/symfony/translation/Provider/AbstractProviderFactory.php b/symfony/translation/Provider/AbstractProviderFactory.php new file mode 100644 index 000000000..17442fde8 --- /dev/null +++ b/symfony/translation/Provider/AbstractProviderFactory.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\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; + +abstract class AbstractProviderFactory implements ProviderFactoryInterface +{ + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); + } + + /** + * @return string[] + */ + abstract protected function getSupportedSchemes(): array; + + protected function getUser(Dsn $dsn): string + { + if (null === $user = $dsn->getUser()) { + throw new IncompleteDsnException('User is not set.', $dsn->getOriginalDsn()); + } + + return $user; + } + + protected function getPassword(Dsn $dsn): string + { + if (null === $password = $dsn->getPassword()) { + throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn()); + } + + return $password; + } +} diff --git a/symfony/translation/Provider/Dsn.php b/symfony/translation/Provider/Dsn.php new file mode 100644 index 000000000..820cabfb3 --- /dev/null +++ b/symfony/translation/Provider/Dsn.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\MissingRequiredOptionException; + +/** + * @author Fabien Potencier + * @author Oskar Stark + */ +final class Dsn +{ + private $scheme; + private $host; + private $user; + private $password; + private $port; + private $path; + private $options; + private $originalDsn; + + public function __construct(string $dsn) + { + $this->originalDsn = $dsn; + + if (false === $parsedDsn = parse_url($dsn)) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn)); + } + + if (!isset($parsedDsn['scheme'])) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn)); + } + $this->scheme = $parsedDsn['scheme']; + + if (!isset($parsedDsn['host'])) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn)); + } + $this->host = $parsedDsn['host']; + + $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; + $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; + $this->port = $parsedDsn['port'] ?? null; + $this->path = $parsedDsn['path'] ?? null; + parse_str($parsedDsn['query'] ?? '', $this->options); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } + + public function getRequiredOption(string $key) + { + if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) { + throw new MissingRequiredOptionException($key); + } + + return $this->options[$key]; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getOriginalDsn(): string + { + return $this->originalDsn; + } +} diff --git a/symfony/translation/Provider/FilteringProvider.php b/symfony/translation/Provider/FilteringProvider.php new file mode 100644 index 000000000..5f970a2e5 --- /dev/null +++ b/symfony/translation/Provider/FilteringProvider.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * Filters domains and locales between the Translator config values and those specific to each provider. + * + * @author Mathieu Santostefano + */ +class FilteringProvider implements ProviderInterface +{ + private $provider; + private $locales; + private $domains; + + public function __construct(ProviderInterface $provider, array $locales, array $domains = []) + { + $this->provider = $provider; + $this->locales = $locales; + $this->domains = $domains; + } + + public function __toString(): string + { + return (string) $this->provider; + } + + /** + * {@inheritdoc} + */ + public function write(TranslatorBagInterface $translatorBag): void + { + $this->provider->write($translatorBag); + } + + public function read(array $domains, array $locales): TranslatorBag + { + $domains = !$this->domains ? $domains : array_intersect($this->domains, $domains); + $locales = array_intersect($this->locales, $locales); + + return $this->provider->read($domains, $locales); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + $this->provider->delete($translatorBag); + } + + public function getDomains(): array + { + return $this->domains; + } +} diff --git a/symfony/translation/Provider/NullProvider.php b/symfony/translation/Provider/NullProvider.php new file mode 100644 index 000000000..f00392ea0 --- /dev/null +++ b/symfony/translation/Provider/NullProvider.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * @author Mathieu Santostefano + */ +class NullProvider implements ProviderInterface +{ + public function __toString(): string + { + return 'null'; + } + + public function write(TranslatorBagInterface $translatorBag, bool $override = false): void + { + } + + public function read(array $domains, array $locales): TranslatorBag + { + return new TranslatorBag(); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + } +} diff --git a/symfony/translation/Provider/NullProviderFactory.php b/symfony/translation/Provider/NullProviderFactory.php new file mode 100644 index 000000000..f350f1602 --- /dev/null +++ b/symfony/translation/Provider/NullProviderFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + */ +final class NullProviderFactory extends AbstractProviderFactory +{ + public function create(Dsn $dsn): ProviderInterface + { + if ('null' === $dsn->getScheme()) { + return new NullProvider(); + } + + throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['null']; + } +} diff --git a/symfony/translation/Provider/ProviderFactoryInterface.php b/symfony/translation/Provider/ProviderFactoryInterface.php new file mode 100644 index 000000000..3fd4494b4 --- /dev/null +++ b/symfony/translation/Provider/ProviderFactoryInterface.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\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +interface ProviderFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): ProviderInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/symfony/translation/Provider/ProviderInterface.php b/symfony/translation/Provider/ProviderInterface.php new file mode 100644 index 000000000..a32193f29 --- /dev/null +++ b/symfony/translation/Provider/ProviderInterface.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\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +interface ProviderInterface +{ + public function __toString(): string; + + /** + * Translations available in the TranslatorBag only must be created. + * Translations available in both the TranslatorBag and on the provider + * must be overwritten. + * Translations available on the provider only must be kept. + */ + public function write(TranslatorBagInterface $translatorBag): void; + + public function read(array $domains, array $locales): TranslatorBag; + + public function delete(TranslatorBagInterface $translatorBag): void; +} diff --git a/symfony/translation/Provider/TranslationProviderCollection.php b/symfony/translation/Provider/TranslationProviderCollection.php new file mode 100644 index 000000000..61ac641cd --- /dev/null +++ b/symfony/translation/Provider/TranslationProviderCollection.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * @author Mathieu Santostefano + */ +final class TranslationProviderCollection +{ + /** + * @var array + */ + private $providers; + + /** + * @param array $providers + */ + public function __construct(iterable $providers) + { + $this->providers = \is_array($providers) ? $providers : iterator_to_array($providers); + } + + public function __toString(): string + { + return '['.implode(',', array_keys($this->providers)).']'; + } + + public function has(string $name): bool + { + return isset($this->providers[$name]); + } + + public function get(string $name): ProviderInterface + { + if (!$this->has($name)) { + throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this)); + } + + return $this->providers[$name]; + } + + public function keys(): array + { + return array_keys($this->providers); + } +} diff --git a/symfony/translation/Provider/TranslationProviderCollectionFactory.php b/symfony/translation/Provider/TranslationProviderCollectionFactory.php new file mode 100644 index 000000000..81db3a5f1 --- /dev/null +++ b/symfony/translation/Provider/TranslationProviderCollectionFactory.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + */ +class TranslationProviderCollectionFactory +{ + private $factories; + private $enabledLocales; + + /** + * @param iterable $factories + */ + public function __construct(iterable $factories, array $enabledLocales) + { + $this->factories = $factories; + $this->enabledLocales = $enabledLocales; + } + + public function fromConfig(array $config): TranslationProviderCollection + { + $providers = []; + foreach ($config as $name => $currentConfig) { + $providers[$name] = $this->fromDsnObject( + new Dsn($currentConfig['dsn']), + !$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'], + !$currentConfig['domains'] ? [] : $currentConfig['domains'] + ); + } + + return new TranslationProviderCollection($providers); + } + + public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return new FilteringProvider($factory->create($dsn), $locales, $domains); + } + } + + throw new UnsupportedSchemeException($dsn); + } +} diff --git a/symfony/translation/PseudoLocalizationTranslator.php b/symfony/translation/PseudoLocalizationTranslator.php new file mode 100644 index 000000000..c769bdad0 --- /dev/null +++ b/symfony/translation/PseudoLocalizationTranslator.php @@ -0,0 +1,368 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * This translator should only be used in a development environment. + */ +final class PseudoLocalizationTranslator implements TranslatorInterface +{ + private const EXPANSION_CHARACTER = '~'; + + private $translator; + private $accents; + private $expansionFactor; + private $brackets; + private $parseHTML; + + /** + * @var string[] + */ + private $localizableHTMLAttributes; + + /** + * Available options: + * * accents: + * type: boolean + * default: true + * description: replace ASCII characters of the translated string with accented versions or similar characters + * example: if true, "foo" => "ƒöö". + * + * * expansion_factor: + * type: float + * default: 1 + * validation: it must be greater than or equal to 1 + * description: expand the translated string by the given factor with spaces and tildes + * example: if 2, "foo" => "~foo ~" + * + * * brackets: + * type: boolean + * default: true + * description: wrap the translated string with brackets + * example: if true, "foo" => "[foo]" + * + * * parse_html: + * type: boolean + * default: false + * description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML + * warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo
bar" => "foo
bar
" + * + * * localizable_html_attributes: + * type: string[] + * default: [] + * description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true + * example: if ["title"], and with the "accents" option set to true, "Profile" => "Þŕöƒîļé" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged. + */ + public function __construct(TranslatorInterface $translator, array $options = []) + { + $this->translator = $translator; + $this->accents = $options['accents'] ?? true; + + if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) { + throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.'); + } + + $this->brackets = $options['brackets'] ?? true; + + $this->parseHTML = $options['parse_html'] ?? false; + if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) { + $this->parseHTML = false; + } + + $this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? []; + } + + /** + * {@inheritdoc} + */ + public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string + { + $trans = ''; + $visibleText = ''; + + foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) { + if ($visible) { + $visibleText .= $text; + } + + if (!$localizable) { + $trans .= $text; + + continue; + } + + $this->addAccents($trans, $text); + } + + $this->expand($trans, $visibleText); + + $this->addBrackets($trans); + + return $trans; + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } + + private function getParts(string $originalTrans): array + { + if (!$this->parseHTML) { + return [[true, true, $originalTrans]]; + } + + $html = mb_encode_numericentity($originalTrans, [0x80, 0xFFFF, 0, 0xFFFF], mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8'); + + $useInternalErrors = libxml_use_internal_errors(true); + + $dom = new \DOMDocument(); + $dom->loadHTML(''.$html.''); + + libxml_clear_errors(); + libxml_use_internal_errors($useInternalErrors); + + return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0)); + } + + private function parseNode(\DOMNode $node): array + { + $parts = []; + + foreach ($node->childNodes as $childNode) { + if (!$childNode instanceof \DOMElement) { + $parts[] = [true, true, $childNode->nodeValue]; + + continue; + } + + $parts[] = [false, false, '<'.$childNode->tagName]; + + /** @var \DOMAttr $attribute */ + foreach ($childNode->attributes as $attribute) { + $parts[] = [false, false, ' '.$attribute->nodeName.'="']; + + $localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true); + foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) { + if ('' === $match) { + continue; + } + + $parts[] = [false, $localizableAttribute && 0 === $i % 2, $match]; + } + + $parts[] = [false, false, '"']; + } + + $parts[] = [false, false, '>']; + + $parts = array_merge($parts, $this->parseNode($childNode, $parts)); + + $parts[] = [false, false, 'tagName.'>']; + } + + return $parts; + } + + private function addAccents(string &$trans, string $text): void + { + $trans .= $this->accents ? strtr($text, [ + ' ' => ' ', + '!' => '¡', + '"' => '″', + '#' => '♯', + '$' => '€', + '%' => '‰', + '&' => '⅋', + '\'' => '´', + '(' => '{', + ')' => '}', + '*' => '⁎', + '+' => '⁺', + ',' => '،', + '-' => '‐', + '.' => '·', + '/' => '⁄', + '0' => '⓪', + '1' => '①', + '2' => '②', + '3' => '③', + '4' => '④', + '5' => '⑤', + '6' => '⑥', + '7' => '⑦', + '8' => '⑧', + '9' => '⑨', + ':' => '∶', + ';' => '⁏', + '<' => '≤', + '=' => '≂', + '>' => '≥', + '?' => '¿', + '@' => '՞', + 'A' => 'Å', + 'B' => 'Ɓ', + 'C' => 'Ç', + 'D' => 'Ð', + 'E' => 'É', + 'F' => 'Ƒ', + 'G' => 'Ĝ', + 'H' => 'Ĥ', + 'I' => 'Î', + 'J' => 'Ĵ', + 'K' => 'Ķ', + 'L' => 'Ļ', + 'M' => 'Ṁ', + 'N' => 'Ñ', + 'O' => 'Ö', + 'P' => 'Þ', + 'Q' => 'Ǫ', + 'R' => 'Ŕ', + 'S' => 'Š', + 'T' => 'Ţ', + 'U' => 'Û', + 'V' => 'Ṽ', + 'W' => 'Ŵ', + 'X' => 'Ẋ', + 'Y' => 'Ý', + 'Z' => 'Ž', + '[' => '⁅', + '\\' => '∖', + ']' => '⁆', + '^' => '˄', + '_' => '‿', + '`' => '‵', + 'a' => 'å', + 'b' => 'ƀ', + 'c' => 'ç', + 'd' => 'ð', + 'e' => 'é', + 'f' => 'ƒ', + 'g' => 'ĝ', + 'h' => 'ĥ', + 'i' => 'î', + 'j' => 'ĵ', + 'k' => 'ķ', + 'l' => 'ļ', + 'm' => 'ɱ', + 'n' => 'ñ', + 'o' => 'ö', + 'p' => 'þ', + 'q' => 'ǫ', + 'r' => 'ŕ', + 's' => 'š', + 't' => 'ţ', + 'u' => 'û', + 'v' => 'ṽ', + 'w' => 'ŵ', + 'x' => 'ẋ', + 'y' => 'ý', + 'z' => 'ž', + '{' => '(', + '|' => '¦', + '}' => ')', + '~' => '˞', + ]) : $text; + } + + private function expand(string &$trans, string $visibleText): void + { + if (1.0 >= $this->expansionFactor) { + return; + } + + $visibleLength = $this->strlen($visibleText); + $missingLength = (int) ceil($visibleLength * $this->expansionFactor) - $visibleLength; + if ($this->brackets) { + $missingLength -= 2; + } + + if (0 >= $missingLength) { + return; + } + + $words = []; + $wordsCount = 0; + foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) { + $wordLength = $this->strlen($word); + + if ($wordLength >= $missingLength) { + continue; + } + + if (!isset($words[$wordLength])) { + $words[$wordLength] = 0; + } + + ++$words[$wordLength]; + ++$wordsCount; + } + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + arsort($words, \SORT_NUMERIC); + + $longestWordLength = max(array_keys($words)); + + while (true) { + $r = mt_rand(1, $wordsCount); + + foreach ($words as $length => $count) { + $r -= $count; + if ($r <= 0) { + break; + } + } + + $trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length); + + $missingLength -= $length + 1; + + if (0 === $missingLength) { + return; + } + + while ($longestWordLength >= $missingLength) { + $wordsCount -= $words[$longestWordLength]; + unset($words[$longestWordLength]); + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + $longestWordLength = max(array_keys($words)); + } + } + } + + private function addBrackets(string &$trans): void + { + if (!$this->brackets) { + return; + } + + $trans = '['.$trans.']'; + } + + private function strlen(string $s): int + { + return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding); + } +} diff --git a/symfony/translation/Reader/TranslationReader.php b/symfony/translation/Reader/TranslationReader.php index 2b9834521..e8e8638a0 100644 --- a/symfony/translation/Reader/TranslationReader.php +++ b/symfony/translation/Reader/TranslationReader.php @@ -25,7 +25,7 @@ class TranslationReader implements TranslationReaderInterface /** * Loaders used for import. * - * @var array + * @var array */ private $loaders = []; @@ -34,7 +34,7 @@ class TranslationReader implements TranslationReaderInterface * * @param string $format The format of the loader */ - public function addLoader($format, LoaderInterface $loader) + public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } @@ -42,7 +42,7 @@ public function addLoader($format, LoaderInterface $loader) /** * {@inheritdoc} */ - public function read($directory, MessageCatalogue $catalogue) + public function read(string $directory, MessageCatalogue $catalogue) { if (!is_dir($directory)) { return; diff --git a/symfony/translation/Reader/TranslationReaderInterface.php b/symfony/translation/Reader/TranslationReaderInterface.php index 0b2ad332a..bc37204f9 100644 --- a/symfony/translation/Reader/TranslationReaderInterface.php +++ b/symfony/translation/Reader/TranslationReaderInterface.php @@ -22,8 +22,6 @@ interface TranslationReaderInterface { /** * Reads translation messages from a directory to the catalogue. - * - * @param string $directory */ - public function read($directory, MessageCatalogue $catalogue); + public function read(string $directory, MessageCatalogue $catalogue); } diff --git a/symfony/translation/Resources/functions.php b/symfony/translation/Resources/functions.php new file mode 100644 index 000000000..901d2f87e --- /dev/null +++ b/symfony/translation/Resources/functions.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\Translation; + +if (!\function_exists(t::class)) { + /** + * @author Nate Wiebe + */ + function t(string $message, array $parameters = [], string $domain = null): TranslatableMessage + { + return new TranslatableMessage($message, $parameters, $domain); + } +} diff --git a/symfony/translation/TranslatableMessage.php b/symfony/translation/TranslatableMessage.php new file mode 100644 index 000000000..282d289c0 --- /dev/null +++ b/symfony/translation/TranslatableMessage.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Nate Wiebe + */ +class TranslatableMessage implements TranslatableInterface +{ + private $message; + private $parameters; + private $domain; + + public function __construct(string $message, array $parameters = [], string $domain = null) + { + $this->message = $message; + $this->parameters = $parameters; + $this->domain = $domain; + } + + public function __toString(): string + { + return $this->getMessage(); + } + + public function getMessage(): string + { + return $this->message; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function getDomain(): ?string + { + return $this->domain; + } + + public function trans(TranslatorInterface $translator, string $locale = null): string + { + return $translator->trans($this->getMessage(), array_map( + static function ($parameter) use ($translator, $locale) { + return $parameter instanceof TranslatableInterface ? $parameter->trans($translator, $locale) : $parameter; + }, + $this->getParameters() + ), $this->getDomain(), $locale); + } +} diff --git a/symfony/translation/Translator.php b/symfony/translation/Translator.php index 5eb0183cb..dc0626093 100644 --- a/symfony/translation/Translator.php +++ b/symfony/translation/Translator.php @@ -15,21 +15,22 @@ use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; -use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface; use Symfony\Component\Translation\Formatter\IntlFormatterInterface; use Symfony\Component\Translation\Formatter\MessageFormatter; use Symfony\Component\Translation\Formatter\MessageFormatterInterface; use Symfony\Component\Translation\Loader\LoaderInterface; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; +// Help opcache.preload discover always-needed symbols +class_exists(MessageCatalogue::class); + /** * @author Fabien Potencier */ -class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface +class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var MessageCatalogueInterface[] @@ -42,7 +43,7 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran private $locale; /** - * @var array + * @var string[] */ private $fallbackLocales = []; @@ -88,13 +89,9 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran /** * @throws InvalidArgumentException If a locale contains invalid characters */ - public function __construct(?string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = []) + public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = []) { - if (null === $locale) { - @trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); - } - - $this->setLocale($locale, false); + $this->setLocale($locale); if (null === $formatter) { $formatter = new MessageFormatter(); @@ -117,7 +114,7 @@ public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFa * * @param string $format The name of the loader (@see addResource()) */ - public function addLoader($format, LoaderInterface $loader) + public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } @@ -127,21 +124,15 @@ public function addLoader($format, LoaderInterface $loader) * * @param string $format The name of the loader (@see addLoader()) * @param mixed $resource The resource name - * @param string $locale The locale - * @param string $domain The domain * * @throws InvalidArgumentException If the locale contains invalid characters */ - public function addResource($format, $resource, $locale, $domain = null) + public function addResource(string $format, $resource, string $locale, string $domain = null) { if (null === $domain) { $domain = 'messages'; } - if (null === $locale) { - @trigger_error(sprintf('Passing "null" to the third argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED); - } - $this->assertValidLocale($locale); $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en'; @@ -157,12 +148,8 @@ public function addResource($format, $resource, $locale, $domain = null) /** * {@inheritdoc} */ - public function setLocale($locale) + public function setLocale(string $locale) { - if (null === $locale && (2 > \func_num_args() || func_get_arg(1))) { - @trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); - } - $this->assertValidLocale($locale); $this->locale = $locale; } @@ -178,6 +165,8 @@ public function getLocale() /** * Sets the fallback locales. * + * @param string[] $locales + * * @throws InvalidArgumentException If a locale contains invalid characters */ public function setFallbackLocales(array $locales) @@ -186,9 +175,6 @@ public function setFallbackLocales(array $locales) $this->catalogues = []; foreach ($locales as $locale) { - if (null === $locale) { - @trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); - } $this->assertValidLocale($locale); } @@ -198,11 +184,9 @@ public function setFallbackLocales(array $locales) /** * Gets the fallback locales. * - * @internal since Symfony 4.2 - * - * @return array The fallback locales + * @internal */ - public function getFallbackLocales() + public function getFallbackLocales(): array { return $this->fallbackLocales; } @@ -210,9 +194,9 @@ public function getFallbackLocales() /** * {@inheritdoc} */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { - if ('' === $id = (string) $id) { + if (null === $id || '' === $id) { return ''; } @@ -231,7 +215,11 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul } } - if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { + $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX); + if ($this->hasIntlFormatter + && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX) + || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len))) + ) { return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters); } @@ -240,47 +228,8 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul /** * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if ('' === $id = (string) $id) { - return ''; - } - - if (!$this->formatter instanceof ChoiceMessageFormatterInterface) { - throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter))); - } - - if (null === $domain) { - $domain = 'messages'; - } - - $catalogue = $this->getCatalogue($locale); - $locale = $catalogue->getLocale(); - while (!$catalogue->defines($id, $domain)) { - if ($cat = $catalogue->getFallbackCatalogue()) { - $catalogue = $cat; - $locale = $catalogue->getLocale(); - } else { - break; - } - } - - if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { - return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, ['%count%' => $number] + $parameters); - } - - return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters); - } - - /** - * {@inheritdoc} - */ - public function getCatalogue($locale = null) + public function getCatalogue(string $locale = null) { if (!$locale) { $locale = $this->getLocale(); @@ -295,20 +244,25 @@ public function getCatalogue($locale = null) return $this->catalogues[$locale]; } + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + /** * Gets the loaders. * - * @return array LoaderInterface[] + * @return LoaderInterface[] */ protected function getLoaders() { return $this->loaders; } - /** - * @param string $locale - */ - protected function loadCatalogue($locale) + protected function loadCatalogue(string $locale) { if (null === $this->cacheDir) { $this->initializeCatalogue($locale); @@ -317,10 +271,7 @@ protected function loadCatalogue($locale) } } - /** - * @param string $locale - */ - protected function initializeCatalogue($locale) + protected function initializeCatalogue(string $locale) { $this->assertValidLocale($locale); @@ -456,7 +407,7 @@ private function loadFallbackCatalogues(string $locale): void } } - protected function computeFallbackLocales($locale) + protected function computeFallbackLocales(string $locale) { if (null === $this->parentLocales) { $this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); @@ -502,13 +453,11 @@ protected function computeFallbackLocales($locale) /** * Asserts that the locale is valid, throws an Exception if not. * - * @param string $locale Locale to tests - * * @throws InvalidArgumentException If the locale contains invalid characters */ - protected function assertValidLocale($locale) + protected function assertValidLocale(string $locale) { - if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', (string) $locale)) { + if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); } } diff --git a/symfony/translation/TranslatorBag.php b/symfony/translation/TranslatorBag.php new file mode 100644 index 000000000..6a4df3c3c --- /dev/null +++ b/symfony/translation/TranslatorBag.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Translation\Catalogue\AbstractOperation; +use Symfony\Component\Translation\Catalogue\TargetOperation; + +final class TranslatorBag implements TranslatorBagInterface +{ + /** @var MessageCatalogue[] */ + private $catalogues = []; + + public function addCatalogue(MessageCatalogue $catalogue): void + { + if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) { + $catalogue->addCatalogue($existingCatalogue); + } + + $this->catalogues[$catalogue->getLocale()] = $catalogue; + } + + public function addBag(TranslatorBagInterface $bag): void + { + foreach ($bag->getCatalogues() as $catalogue) { + $this->addCatalogue($catalogue); + } + } + + /** + * {@inheritdoc} + */ + public function getCatalogue(string $locale = null): MessageCatalogueInterface + { + if (null === $locale || !isset($this->catalogues[$locale])) { + $this->catalogues[$locale] = new MessageCatalogue($locale); + } + + return $this->catalogues[$locale]; + } + + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + + public function diff(TranslatorBagInterface $diffBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) { + $diff->addCatalogue($catalogue); + + continue; + } + + $operation = new TargetOperation($diffCatalogue, $catalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH); + $newCatalogue = new MessageCatalogue($locale); + + foreach ($catalogue->getDomains() as $domain) { + $newCatalogue->add($operation->getNewMessages($domain), $domain); + } + + $diff->addCatalogue($newCatalogue); + } + + return $diff; + } + + public function intersect(TranslatorBagInterface $intersectBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) { + continue; + } + + $operation = new TargetOperation($catalogue, $intersectCatalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH); + $obsoleteCatalogue = new MessageCatalogue($locale); + + foreach ($operation->getDomains() as $domain) { + $obsoleteCatalogue->add( + array_diff($operation->getMessages($domain), $operation->getNewMessages($domain)), + $domain + ); + } + + $diff->addCatalogue($obsoleteCatalogue); + } + + return $diff; + } +} diff --git a/symfony/translation/TranslatorBagInterface.php b/symfony/translation/TranslatorBagInterface.php index 5e49e2ddc..422897735 100644 --- a/symfony/translation/TranslatorBagInterface.php +++ b/symfony/translation/TranslatorBagInterface.php @@ -16,6 +16,8 @@ /** * TranslatorBagInterface. * + * @method MessageCatalogueInterface[] getCatalogues() Returns all catalogues of the instance + * * @author Abdellatif Ait boudad */ interface TranslatorBagInterface @@ -29,5 +31,5 @@ interface TranslatorBagInterface * * @throws InvalidArgumentException If the locale contains invalid characters */ - public function getCatalogue($locale = null); + public function getCatalogue(string $locale = null); } diff --git a/symfony/translation/TranslatorInterface.php b/symfony/translation/TranslatorInterface.php deleted file mode 100644 index f677d2455..000000000 --- a/symfony/translation/TranslatorInterface.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Contracts\Translation\LocaleAwareInterface; - -/** - * TranslatorInterface. - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use Symfony\Contracts\Translation\TranslatorInterface instead - */ -interface TranslatorInterface extends LocaleAwareInterface -{ - /** - * Translates the given message. - * - * @param string $id The message id (may also be an object that can be cast to string) - * @param array $parameters An array of parameters for the message - * @param string|null $domain The domain for the message or null to use the default - * @param string|null $locale The locale or null to use the default - * - * @return string The translated string - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function trans($id, array $parameters = [], $domain = null, $locale = null); - - /** - * Translates the given choice message by choosing a translation according to a number. - * - * @param string $id The message id (may also be an object that can be cast to string) - * @param int $number The number to use to find the index of the message - * @param array $parameters An array of parameters for the message - * @param string|null $domain The domain for the message or null to use the default - * @param string|null $locale The locale or null to use the default - * - * @return string The translated string - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null); - - /** - * Sets the current locale. - * - * @param string $locale The locale - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function setLocale($locale); - - /** - * Returns the current locale. - * - * @return string The locale - */ - public function getLocale(); -} diff --git a/symfony/translation/Util/ArrayConverter.php b/symfony/translation/Util/ArrayConverter.php index 22c602e71..f69c2e3c6 100644 --- a/symfony/translation/Util/ArrayConverter.php +++ b/symfony/translation/Util/ArrayConverter.php @@ -31,7 +31,7 @@ class ArrayConverter * * @param array $messages Linear messages array * - * @return array Tree-like messages array + * @return array */ public static function expandToTree(array $messages) { @@ -84,7 +84,7 @@ private static function &getElementByPath(array &$tree, array $parts) return $elem; } - private static function cancelExpand(array &$tree, $prefix, array $node) + private static function cancelExpand(array &$tree, string $prefix, array $node) { $prefix .= '.'; diff --git a/symfony/translation/Writer/TranslationWriter.php b/symfony/translation/Writer/TranslationWriter.php index 83f6fd615..96d608f07 100644 --- a/symfony/translation/Writer/TranslationWriter.php +++ b/symfony/translation/Writer/TranslationWriter.php @@ -23,34 +23,19 @@ */ class TranslationWriter implements TranslationWriterInterface { + /** + * @var array + */ private $dumpers = []; /** * Adds a dumper to the writer. - * - * @param string $format The format of the dumper */ - public function addDumper($format, DumperInterface $dumper) + public function addDumper(string $format, DumperInterface $dumper) { $this->dumpers[$format] = $dumper; } - /** - * Disables dumper backup. - * - * @deprecated since Symfony 4.1 - */ - public function disableBackup() - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), \E_USER_DEPRECATED); - - foreach ($this->dumpers as $dumper) { - if (method_exists($dumper, 'setBackup')) { - $dumper->setBackup(false); - } - } - } - /** * Obtains the list of supported formats. * @@ -69,7 +54,7 @@ public function getFormats() * * @throws InvalidArgumentException */ - public function write(MessageCatalogue $catalogue, $format, $options = []) + public function write(MessageCatalogue $catalogue, string $format, array $options = []) { if (!isset($this->dumpers[$format])) { throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format)); diff --git a/symfony/translation/Writer/TranslationWriterInterface.php b/symfony/translation/Writer/TranslationWriterInterface.php index f7c56bed0..43213097e 100644 --- a/symfony/translation/Writer/TranslationWriterInterface.php +++ b/symfony/translation/Writer/TranslationWriterInterface.php @@ -29,5 +29,5 @@ interface TranslationWriterInterface * * @throws InvalidArgumentException */ - public function write(MessageCatalogue $catalogue, $format, $options = []); + public function write(MessageCatalogue $catalogue, string $format, array $options = []); }