From cd14c81cbc8f5ddda0701210fbc5ee8239c4455b Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Tue, 7 Oct 2025 11:38:46 +0200 Subject: [PATCH 01/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20Add=20Symfony/forms=20Update=20i?= =?UTF-8?q?Top=20Controller=20to=20branch=20to=20Symfony/forms=20Add=20tem?= =?UTF-8?q?plates=20for=20ibo--=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + composer.lock | 433 ++++++++++++++++- lib/composer/autoload_classmap.php | 327 +++++++++++++ lib/composer/autoload_files.php | 5 +- lib/composer/autoload_psr4.php | 5 + lib/composer/autoload_static.php | 357 +++++++++++++- lib/composer/installed.json | 446 ++++++++++++++++++ lib/composer/installed.php | 49 +- .../TwigBase/Controller/Controller.php | 46 +- .../forms/itop_base_layout.html.twig | 4 + .../forms/itop_console_layout.twig | 23 + 11 files changed, 1685 insertions(+), 11 deletions(-) create mode 100644 templates/application/forms/itop_base_layout.html.twig create mode 100644 templates/application/forms/itop_console_layout.twig diff --git a/composer.json b/composer.json index ffb7bd452e..1e83a16e23 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "soundasleep/html2text": "~2.1", "symfony/console": "~6.4.0", "symfony/dotenv": "~6.4.0", + "symfony/form": "^6.4", "symfony/framework-bundle": "~6.4.0", "symfony/http-foundation": "~6.4.0", "symfony/http-kernel": "~6.4.0", diff --git a/composer.lock b/composer.lock index 95ce06c88b..d7d3107866 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": "e297f28f87219c0cc7ffb4f4a7ee5449", + "content-hash": "104c5ecdb6e4797332831f4e7ba3e3ae", "packages": [ { "name": "apereo/phpcas", @@ -2748,6 +2748,107 @@ ], "time": "2025-07-15T12:02:45+00:00" }, + { + "name": "symfony/form", + "version": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "b40cdbe70be9274ea807ef61da7d0f8d1c70dc51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/b40cdbe70be9274ea807ef61da7d0f8d1c70dc51", + "reference": "b40cdbe70be9274ea807ef61da7d0f8d1c70dc51", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", + "symfony/error-handler": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.3" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.2|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/form/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-20T07:40:41+00:00" + }, { "name": "symfony/framework-bundle", "version": "v6.4.25", @@ -3273,6 +3374,77 @@ ], "time": "2025-07-15T12:02:45+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v6.4.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d28e7e2db8a73e9511df892d36445f61314bbebe", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T17:06:28+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -3438,6 +3610,94 @@ ], "time": "2025-06-27T09:58:17+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-20T22:24:30+00:00" + }, { "name": "symfony/polyfill-intl-idn", "version": "v1.33.0", @@ -3775,6 +4035,177 @@ ], "time": "2025-07-08T02:45:35+00:00" }, + { + "name": "symfony/property-access", + "version": "v6.4.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "fedc771326d4978a7d3167fa009a509b06a2e168" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/fedc771326d4978a7d3167fa009a509b06a2e168", + "reference": "fedc771326d4978a7d3167fa009a509b06a2e168", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/property-info": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-12T15:42:57+00:00" + }, + { + "name": "symfony/property-info", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "1056ae3621eeddd78d7c5ec074f1c1784324eec6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/1056ae3621eeddd78d7c5ec074f1c1784324eec6", + "reference": "1056ae3621eeddd78d7c5ec074f1c1784324eec6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-14T16:38:25+00:00" + }, { "name": "symfony/routing", "version": "v6.4.24", diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 8c7dad6c24..026659f548 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -128,6 +128,7 @@ 'CharConcatWSExpression' => $baseDir . '/core/oql/expression.class.inc.php', 'CheckStopWatchThresholds' => $baseDir . '/core/ormstopwatch.class.inc.php', 'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', + 'Collator' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php', 'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/Application/Branding.php', 'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php', 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php', @@ -143,6 +144,8 @@ 'Combodo\\iTop\\Application\\Search\\CriterionParser' => $baseDir . '/sources/Application/Search/criterionparser.class.inc.php', 'Combodo\\iTop\\Application\\Search\\SearchForm' => $baseDir . '/sources/Application/Search/searchform.class.inc.php', 'Combodo\\iTop\\Application\\Status\\Status' => $baseDir . '/sources/Application/Status/Status.php', + 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormController' => $baseDir . '/sources/Application/Symfony/Poc/BaseForm/BaseFormController.php', + 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormType' => $baseDir . '/sources/Application/Symfony/Poc/BaseForm/BaseFormType.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\Controller' => $baseDir . '/sources/Application/TwigBase/Controller/Controller.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\PageNotFoundException' => $baseDir . '/application/exceptions/PageNotFoundException.php', 'Combodo\\iTop\\Application\\TwigBase\\Twig\\Extension' => $baseDir . '/sources/Application/TwigBase/Twig/Extension.php', @@ -840,6 +843,7 @@ 'InputOutputTask' => $baseDir . '/application/iotask.class.inc.php', 'IntervalExpression' => $baseDir . '/core/oql/expression.class.inc.php', 'IntervalOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', + 'IntlDateFormatter' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/IntlDateFormatter.php', 'Introspection' => $baseDir . '/core/introspection.class.inc.php', 'InvalidConfigParamException' => $baseDir . '/application/exceptions/InvalidConfigParamException.php', 'InvalidExternalKeyValueException' => $baseDir . '/application/exceptions/InvalidExternalKeyValueException.php', @@ -881,6 +885,7 @@ 'League\\OAuth2\\Client\\Tool\\RequiredParameterTrait' => $vendorDir . '/league/oauth2-client/src/Tool/RequiredParameterTrait.php', 'ListExpression' => $baseDir . '/core/oql/expression.class.inc.php', 'ListOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', + 'Locale' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/Locale.php', 'LogAPI' => $baseDir . '/core/log.class.inc.php', 'LogChannels' => $baseDir . '/core/log.class.inc.php', 'LogFileNameBuilderFactory' => $baseDir . '/core/log.class.inc.php', @@ -914,6 +919,7 @@ 'NewsroomProviderBase' => $baseDir . '/application/newsroomprovider.class.inc.php', 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'NotYetEvaluatedExpression' => $baseDir . '/core/oql/expression.class.inc.php', + 'NumberFormatter' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/NumberFormatter.php', 'OQLActualClassTreeResolver' => $baseDir . '/core/oqlactualclasstreeresolver.class.inc.php', 'OQLClassNode' => $baseDir . '/core/oqlclassnode.class.inc.php', 'OQLClassTreeBuilder' => $baseDir . '/core/oqlclasstreebuilder.class.inc.php', @@ -2192,6 +2198,241 @@ 'Symfony\\Component\\Finder\\Iterator\\SortableIterator' => $vendorDir . '/symfony/finder/Iterator/SortableIterator.php', 'Symfony\\Component\\Finder\\Iterator\\VcsIgnoredFilterIterator' => $vendorDir . '/symfony/finder/Iterator/VcsIgnoredFilterIterator.php', 'Symfony\\Component\\Finder\\SplFileInfo' => $vendorDir . '/symfony/finder/SplFileInfo.php', + 'Symfony\\Component\\Form\\AbstractExtension' => $vendorDir . '/symfony/form/AbstractExtension.php', + 'Symfony\\Component\\Form\\AbstractRendererEngine' => $vendorDir . '/symfony/form/AbstractRendererEngine.php', + 'Symfony\\Component\\Form\\AbstractType' => $vendorDir . '/symfony/form/AbstractType.php', + 'Symfony\\Component\\Form\\AbstractTypeExtension' => $vendorDir . '/symfony/form/AbstractTypeExtension.php', + 'Symfony\\Component\\Form\\Button' => $vendorDir . '/symfony/form/Button.php', + 'Symfony\\Component\\Form\\ButtonBuilder' => $vendorDir . '/symfony/form/ButtonBuilder.php', + 'Symfony\\Component\\Form\\ButtonTypeInterface' => $vendorDir . '/symfony/form/ButtonTypeInterface.php', + 'Symfony\\Component\\Form\\CallbackTransformer' => $vendorDir . '/symfony/form/CallbackTransformer.php', + 'Symfony\\Component\\Form\\ChoiceList\\ArrayChoiceList' => $vendorDir . '/symfony/form/ChoiceList/ArrayChoiceList.php', + 'Symfony\\Component\\Form\\ChoiceList\\ChoiceList' => $vendorDir . '/symfony/form/ChoiceList/ChoiceList.php', + 'Symfony\\Component\\Form\\ChoiceList\\ChoiceListInterface' => $vendorDir . '/symfony/form/ChoiceList/ChoiceListInterface.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\AbstractStaticOption' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/AbstractStaticOption.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceAttr' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceAttr.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceFieldName' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceFieldName.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceFilter' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceFilter.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceLabel' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceLabel.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceLoader' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceTranslationParameters' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceTranslationParameters.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceValue' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/ChoiceValue.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\GroupBy' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/GroupBy.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\PreferredChoice' => $vendorDir . '/symfony/form/ChoiceList/Factory/Cache/PreferredChoice.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\CachingFactoryDecorator' => $vendorDir . '/symfony/form/ChoiceList/Factory/CachingFactoryDecorator.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\ChoiceListFactoryInterface' => $vendorDir . '/symfony/form/ChoiceList/Factory/ChoiceListFactoryInterface.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\DefaultChoiceListFactory' => $vendorDir . '/symfony/form/ChoiceList/Factory/DefaultChoiceListFactory.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\PropertyAccessDecorator' => $vendorDir . '/symfony/form/ChoiceList/Factory/PropertyAccessDecorator.php', + 'Symfony\\Component\\Form\\ChoiceList\\LazyChoiceList' => $vendorDir . '/symfony/form/ChoiceList/LazyChoiceList.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader' => $vendorDir . '/symfony/form/ChoiceList/Loader/AbstractChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\CallbackChoiceLoader' => $vendorDir . '/symfony/form/ChoiceList/Loader/CallbackChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface' => $vendorDir . '/symfony/form/ChoiceList/Loader/ChoiceLoaderInterface.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\FilterChoiceLoaderDecorator' => $vendorDir . '/symfony/form/ChoiceList/Loader/FilterChoiceLoaderDecorator.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\IntlCallbackChoiceLoader' => $vendorDir . '/symfony/form/ChoiceList/Loader/IntlCallbackChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\View\\ChoiceGroupView' => $vendorDir . '/symfony/form/ChoiceList/View/ChoiceGroupView.php', + 'Symfony\\Component\\Form\\ChoiceList\\View\\ChoiceListView' => $vendorDir . '/symfony/form/ChoiceList/View/ChoiceListView.php', + 'Symfony\\Component\\Form\\ChoiceList\\View\\ChoiceView' => $vendorDir . '/symfony/form/ChoiceList/View/ChoiceView.php', + 'Symfony\\Component\\Form\\ClearableErrorsInterface' => $vendorDir . '/symfony/form/ClearableErrorsInterface.php', + 'Symfony\\Component\\Form\\ClickableInterface' => $vendorDir . '/symfony/form/ClickableInterface.php', + 'Symfony\\Component\\Form\\Command\\DebugCommand' => $vendorDir . '/symfony/form/Command/DebugCommand.php', + 'Symfony\\Component\\Form\\Console\\Descriptor\\Descriptor' => $vendorDir . '/symfony/form/Console/Descriptor/Descriptor.php', + 'Symfony\\Component\\Form\\Console\\Descriptor\\JsonDescriptor' => $vendorDir . '/symfony/form/Console/Descriptor/JsonDescriptor.php', + 'Symfony\\Component\\Form\\Console\\Descriptor\\TextDescriptor' => $vendorDir . '/symfony/form/Console/Descriptor/TextDescriptor.php', + 'Symfony\\Component\\Form\\Console\\Helper\\DescriptorHelper' => $vendorDir . '/symfony/form/Console/Helper/DescriptorHelper.php', + 'Symfony\\Component\\Form\\DataAccessorInterface' => $vendorDir . '/symfony/form/DataAccessorInterface.php', + 'Symfony\\Component\\Form\\DataMapperInterface' => $vendorDir . '/symfony/form/DataMapperInterface.php', + 'Symfony\\Component\\Form\\DataTransformerInterface' => $vendorDir . '/symfony/form/DataTransformerInterface.php', + 'Symfony\\Component\\Form\\DependencyInjection\\FormPass' => $vendorDir . '/symfony/form/DependencyInjection/FormPass.php', + 'Symfony\\Component\\Form\\Event\\PostSetDataEvent' => $vendorDir . '/symfony/form/Event/PostSetDataEvent.php', + 'Symfony\\Component\\Form\\Event\\PostSubmitEvent' => $vendorDir . '/symfony/form/Event/PostSubmitEvent.php', + 'Symfony\\Component\\Form\\Event\\PreSetDataEvent' => $vendorDir . '/symfony/form/Event/PreSetDataEvent.php', + 'Symfony\\Component\\Form\\Event\\PreSubmitEvent' => $vendorDir . '/symfony/form/Event/PreSubmitEvent.php', + 'Symfony\\Component\\Form\\Event\\SubmitEvent' => $vendorDir . '/symfony/form/Event/SubmitEvent.php', + 'Symfony\\Component\\Form\\Exception\\AccessException' => $vendorDir . '/symfony/form/Exception/AccessException.php', + 'Symfony\\Component\\Form\\Exception\\AlreadySubmittedException' => $vendorDir . '/symfony/form/Exception/AlreadySubmittedException.php', + 'Symfony\\Component\\Form\\Exception\\BadMethodCallException' => $vendorDir . '/symfony/form/Exception/BadMethodCallException.php', + 'Symfony\\Component\\Form\\Exception\\ErrorMappingException' => $vendorDir . '/symfony/form/Exception/ErrorMappingException.php', + 'Symfony\\Component\\Form\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/form/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Form\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/form/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Form\\Exception\\InvalidConfigurationException' => $vendorDir . '/symfony/form/Exception/InvalidConfigurationException.php', + 'Symfony\\Component\\Form\\Exception\\LogicException' => $vendorDir . '/symfony/form/Exception/LogicException.php', + 'Symfony\\Component\\Form\\Exception\\OutOfBoundsException' => $vendorDir . '/symfony/form/Exception/OutOfBoundsException.php', + 'Symfony\\Component\\Form\\Exception\\RuntimeException' => $vendorDir . '/symfony/form/Exception/RuntimeException.php', + 'Symfony\\Component\\Form\\Exception\\StringCastException' => $vendorDir . '/symfony/form/Exception/StringCastException.php', + 'Symfony\\Component\\Form\\Exception\\TransformationFailedException' => $vendorDir . '/symfony/form/Exception/TransformationFailedException.php', + 'Symfony\\Component\\Form\\Exception\\UnexpectedTypeException' => $vendorDir . '/symfony/form/Exception/UnexpectedTypeException.php', + 'Symfony\\Component\\Form\\Extension\\Core\\CoreExtension' => $vendorDir . '/symfony/form/Extension/Core/CoreExtension.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataAccessor\\CallbackAccessor' => $vendorDir . '/symfony/form/Extension/Core/DataAccessor/CallbackAccessor.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataAccessor\\ChainAccessor' => $vendorDir . '/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataAccessor\\PropertyPathAccessor' => $vendorDir . '/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataMapper\\CheckboxListMapper' => $vendorDir . '/symfony/form/Extension/Core/DataMapper/CheckboxListMapper.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataMapper\\DataMapper' => $vendorDir . '/symfony/form/Extension/Core/DataMapper/DataMapper.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataMapper\\RadioListMapper' => $vendorDir . '/symfony/form/Extension/Core/DataMapper/RadioListMapper.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ArrayToPartsTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/ArrayToPartsTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\BaseDateTimeTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/BaseDateTimeTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\BooleanToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/BooleanToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ChoiceToValueTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ChoicesToValuesTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DataTransformerChain' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DataTransformerChain.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateIntervalToArrayTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateIntervalToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeImmutableToDateTimeTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToArrayTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToHtml5LocalDateTimeTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToLocalizedStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToRfc3339Transformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToTimestampTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeZoneToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\IntegerToLocalizedStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\IntlTimeZoneToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\MoneyToLocalizedStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\NumberToLocalizedStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\PercentToLocalizedStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\StringToFloatTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/StringToFloatTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\UlidToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/UlidToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\UuidToStringTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/UuidToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ValueToDuplicatesTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\WeekToArrayTransformer' => $vendorDir . '/symfony/form/Extension/Core/DataTransformer/WeekToArrayTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\FixUrlProtocolListener' => $vendorDir . '/symfony/form/Extension/Core/EventListener/FixUrlProtocolListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\MergeCollectionListener' => $vendorDir . '/symfony/form/Extension/Core/EventListener/MergeCollectionListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\ResizeFormListener' => $vendorDir . '/symfony/form/Extension/Core/EventListener/ResizeFormListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\TransformationFailureListener' => $vendorDir . '/symfony/form/Extension/Core/EventListener/TransformationFailureListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\TrimListener' => $vendorDir . '/symfony/form/Extension/Core/EventListener/TrimListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\BaseType' => $vendorDir . '/symfony/form/Extension/Core/Type/BaseType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType' => $vendorDir . '/symfony/form/Extension/Core/Type/BirthdayType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType' => $vendorDir . '/symfony/form/Extension/Core/Type/ButtonType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType' => $vendorDir . '/symfony/form/Extension/Core/Type/CheckboxType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType' => $vendorDir . '/symfony/form/Extension/Core/Type/ChoiceType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType' => $vendorDir . '/symfony/form/Extension/Core/Type/CollectionType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ColorType' => $vendorDir . '/symfony/form/Extension/Core/Type/ColorType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CountryType' => $vendorDir . '/symfony/form/Extension/Core/Type/CountryType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CurrencyType' => $vendorDir . '/symfony/form/Extension/Core/Type/CurrencyType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateIntervalType' => $vendorDir . '/symfony/form/Extension/Core/Type/DateIntervalType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType' => $vendorDir . '/symfony/form/Extension/Core/Type/DateTimeType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType' => $vendorDir . '/symfony/form/Extension/Core/Type/DateType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType' => $vendorDir . '/symfony/form/Extension/Core/Type/EmailType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\EnumType' => $vendorDir . '/symfony/form/Extension/Core/Type/EnumType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType' => $vendorDir . '/symfony/form/Extension/Core/Type/FileType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType' => $vendorDir . '/symfony/form/Extension/Core/Type/FormType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType' => $vendorDir . '/symfony/form/Extension/Core/Type/HiddenType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType' => $vendorDir . '/symfony/form/Extension/Core/Type/IntegerType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\LanguageType' => $vendorDir . '/symfony/form/Extension/Core/Type/LanguageType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\LocaleType' => $vendorDir . '/symfony/form/Extension/Core/Type/LocaleType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\MoneyType' => $vendorDir . '/symfony/form/Extension/Core/Type/MoneyType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType' => $vendorDir . '/symfony/form/Extension/Core/Type/NumberType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType' => $vendorDir . '/symfony/form/Extension/Core/Type/PasswordType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\PercentType' => $vendorDir . '/symfony/form/Extension/Core/Type/PercentType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\RadioType' => $vendorDir . '/symfony/form/Extension/Core/Type/RadioType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\RangeType' => $vendorDir . '/symfony/form/Extension/Core/Type/RangeType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType' => $vendorDir . '/symfony/form/Extension/Core/Type/RepeatedType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ResetType' => $vendorDir . '/symfony/form/Extension/Core/Type/ResetType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType' => $vendorDir . '/symfony/form/Extension/Core/Type/SearchType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType' => $vendorDir . '/symfony/form/Extension/Core/Type/SubmitType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TelType' => $vendorDir . '/symfony/form/Extension/Core/Type/TelType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType' => $vendorDir . '/symfony/form/Extension/Core/Type/TextType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType' => $vendorDir . '/symfony/form/Extension/Core/Type/TextareaType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TimeType' => $vendorDir . '/symfony/form/Extension/Core/Type/TimeType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TimezoneType' => $vendorDir . '/symfony/form/Extension/Core/Type/TimezoneType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TransformationFailureExtension' => $vendorDir . '/symfony/form/Extension/Core/Type/TransformationFailureExtension.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UlidType' => $vendorDir . '/symfony/form/Extension/Core/Type/UlidType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType' => $vendorDir . '/symfony/form/Extension/Core/Type/UrlType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UuidType' => $vendorDir . '/symfony/form/Extension/Core/Type/UuidType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\WeekType' => $vendorDir . '/symfony/form/Extension/Core/Type/WeekType.php', + 'Symfony\\Component\\Form\\Extension\\Csrf\\CsrfExtension' => $vendorDir . '/symfony/form/Extension/Csrf/CsrfExtension.php', + 'Symfony\\Component\\Form\\Extension\\Csrf\\EventListener\\CsrfValidationListener' => $vendorDir . '/symfony/form/Extension/Csrf/EventListener/CsrfValidationListener.php', + 'Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension' => $vendorDir . '/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\DataCollectorExtension' => $vendorDir . '/symfony/form/Extension/DataCollector/DataCollectorExtension.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\EventListener\\DataCollectorListener' => $vendorDir . '/symfony/form/Extension/DataCollector/EventListener/DataCollectorListener.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataCollector' => $vendorDir . '/symfony/form/Extension/DataCollector/FormDataCollector.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataCollectorInterface' => $vendorDir . '/symfony/form/Extension/DataCollector/FormDataCollectorInterface.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataExtractor' => $vendorDir . '/symfony/form/Extension/DataCollector/FormDataExtractor.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataExtractorInterface' => $vendorDir . '/symfony/form/Extension/DataCollector/FormDataExtractorInterface.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\Proxy\\ResolvedTypeDataCollectorProxy' => $vendorDir . '/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\Proxy\\ResolvedTypeFactoryDataCollectorProxy' => $vendorDir . '/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\Type\\DataCollectorTypeExtension' => $vendorDir . '/symfony/form/Extension/DataCollector/Type/DataCollectorTypeExtension.php', + 'Symfony\\Component\\Form\\Extension\\DependencyInjection\\DependencyInjectionExtension' => $vendorDir . '/symfony/form/Extension/DependencyInjection/DependencyInjectionExtension.php', + 'Symfony\\Component\\Form\\Extension\\HtmlSanitizer\\HtmlSanitizerExtension' => $vendorDir . '/symfony/form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php', + 'Symfony\\Component\\Form\\Extension\\HtmlSanitizer\\Type\\TextTypeHtmlSanitizerExtension' => $vendorDir . '/symfony/form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php', + 'Symfony\\Component\\Form\\Extension\\HttpFoundation\\HttpFoundationExtension' => $vendorDir . '/symfony/form/Extension/HttpFoundation/HttpFoundationExtension.php', + 'Symfony\\Component\\Form\\Extension\\HttpFoundation\\HttpFoundationRequestHandler' => $vendorDir . '/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php', + 'Symfony\\Component\\Form\\Extension\\HttpFoundation\\Type\\FormTypeHttpFoundationExtension' => $vendorDir . '/symfony/form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\EventListener\\PasswordHasherListener' => $vendorDir . '/symfony/form/Extension/PasswordHasher/EventListener/PasswordHasherListener.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\PasswordHasherExtension' => $vendorDir . '/symfony/form/Extension/PasswordHasher/PasswordHasherExtension.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\Type\\FormTypePasswordHasherExtension' => $vendorDir . '/symfony/form/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\Type\\PasswordTypePasswordHasherExtension' => $vendorDir . '/symfony/form/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Constraints\\Form' => $vendorDir . '/symfony/form/Extension/Validator/Constraints/Form.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Constraints\\FormValidator' => $vendorDir . '/symfony/form/Extension/Validator/Constraints/FormValidator.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\EventListener\\ValidationListener' => $vendorDir . '/symfony/form/Extension/Validator/EventListener/ValidationListener.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\BaseValidatorExtension' => $vendorDir . '/symfony/form/Extension/Validator/Type/BaseValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\FormTypeValidatorExtension' => $vendorDir . '/symfony/form/Extension/Validator/Type/FormTypeValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\RepeatedTypeValidatorExtension' => $vendorDir . '/symfony/form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\SubmitTypeValidatorExtension' => $vendorDir . '/symfony/form/Extension/Validator/Type/SubmitTypeValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\UploadValidatorExtension' => $vendorDir . '/symfony/form/Extension/Validator/Type/UploadValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ValidatorExtension' => $vendorDir . '/symfony/form/Extension/Validator/ValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ValidatorTypeGuesser' => $vendorDir . '/symfony/form/Extension/Validator/ValidatorTypeGuesser.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\MappingRule' => $vendorDir . '/symfony/form/Extension/Validator/ViolationMapper/MappingRule.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\RelativePath' => $vendorDir . '/symfony/form/Extension/Validator/ViolationMapper/RelativePath.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationMapper' => $vendorDir . '/symfony/form/Extension/Validator/ViolationMapper/ViolationMapper.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationMapperInterface' => $vendorDir . '/symfony/form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationPath' => $vendorDir . '/symfony/form/Extension/Validator/ViolationMapper/ViolationPath.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationPathIterator' => $vendorDir . '/symfony/form/Extension/Validator/ViolationMapper/ViolationPathIterator.php', + 'Symfony\\Component\\Form\\FileUploadError' => $vendorDir . '/symfony/form/FileUploadError.php', + 'Symfony\\Component\\Form\\Form' => $vendorDir . '/symfony/form/Form.php', + 'Symfony\\Component\\Form\\FormBuilder' => $vendorDir . '/symfony/form/FormBuilder.php', + 'Symfony\\Component\\Form\\FormBuilderInterface' => $vendorDir . '/symfony/form/FormBuilderInterface.php', + 'Symfony\\Component\\Form\\FormConfigBuilder' => $vendorDir . '/symfony/form/FormConfigBuilder.php', + 'Symfony\\Component\\Form\\FormConfigBuilderInterface' => $vendorDir . '/symfony/form/FormConfigBuilderInterface.php', + 'Symfony\\Component\\Form\\FormConfigInterface' => $vendorDir . '/symfony/form/FormConfigInterface.php', + 'Symfony\\Component\\Form\\FormError' => $vendorDir . '/symfony/form/FormError.php', + 'Symfony\\Component\\Form\\FormErrorIterator' => $vendorDir . '/symfony/form/FormErrorIterator.php', + 'Symfony\\Component\\Form\\FormEvent' => $vendorDir . '/symfony/form/FormEvent.php', + 'Symfony\\Component\\Form\\FormEvents' => $vendorDir . '/symfony/form/FormEvents.php', + 'Symfony\\Component\\Form\\FormExtensionInterface' => $vendorDir . '/symfony/form/FormExtensionInterface.php', + 'Symfony\\Component\\Form\\FormFactory' => $vendorDir . '/symfony/form/FormFactory.php', + 'Symfony\\Component\\Form\\FormFactoryBuilder' => $vendorDir . '/symfony/form/FormFactoryBuilder.php', + 'Symfony\\Component\\Form\\FormFactoryBuilderInterface' => $vendorDir . '/symfony/form/FormFactoryBuilderInterface.php', + 'Symfony\\Component\\Form\\FormFactoryInterface' => $vendorDir . '/symfony/form/FormFactoryInterface.php', + 'Symfony\\Component\\Form\\FormInterface' => $vendorDir . '/symfony/form/FormInterface.php', + 'Symfony\\Component\\Form\\FormRegistry' => $vendorDir . '/symfony/form/FormRegistry.php', + 'Symfony\\Component\\Form\\FormRegistryInterface' => $vendorDir . '/symfony/form/FormRegistryInterface.php', + 'Symfony\\Component\\Form\\FormRenderer' => $vendorDir . '/symfony/form/FormRenderer.php', + 'Symfony\\Component\\Form\\FormRendererEngineInterface' => $vendorDir . '/symfony/form/FormRendererEngineInterface.php', + 'Symfony\\Component\\Form\\FormRendererInterface' => $vendorDir . '/symfony/form/FormRendererInterface.php', + 'Symfony\\Component\\Form\\FormTypeExtensionInterface' => $vendorDir . '/symfony/form/FormTypeExtensionInterface.php', + 'Symfony\\Component\\Form\\FormTypeGuesserChain' => $vendorDir . '/symfony/form/FormTypeGuesserChain.php', + 'Symfony\\Component\\Form\\FormTypeGuesserInterface' => $vendorDir . '/symfony/form/FormTypeGuesserInterface.php', + 'Symfony\\Component\\Form\\FormTypeInterface' => $vendorDir . '/symfony/form/FormTypeInterface.php', + 'Symfony\\Component\\Form\\FormView' => $vendorDir . '/symfony/form/FormView.php', + 'Symfony\\Component\\Form\\Forms' => $vendorDir . '/symfony/form/Forms.php', + 'Symfony\\Component\\Form\\Guess\\Guess' => $vendorDir . '/symfony/form/Guess/Guess.php', + 'Symfony\\Component\\Form\\Guess\\TypeGuess' => $vendorDir . '/symfony/form/Guess/TypeGuess.php', + 'Symfony\\Component\\Form\\Guess\\ValueGuess' => $vendorDir . '/symfony/form/Guess/ValueGuess.php', + 'Symfony\\Component\\Form\\NativeRequestHandler' => $vendorDir . '/symfony/form/NativeRequestHandler.php', + 'Symfony\\Component\\Form\\PreloadedExtension' => $vendorDir . '/symfony/form/PreloadedExtension.php', + 'Symfony\\Component\\Form\\RequestHandlerInterface' => $vendorDir . '/symfony/form/RequestHandlerInterface.php', + 'Symfony\\Component\\Form\\ResolvedFormType' => $vendorDir . '/symfony/form/ResolvedFormType.php', + 'Symfony\\Component\\Form\\ResolvedFormTypeFactory' => $vendorDir . '/symfony/form/ResolvedFormTypeFactory.php', + 'Symfony\\Component\\Form\\ResolvedFormTypeFactoryInterface' => $vendorDir . '/symfony/form/ResolvedFormTypeFactoryInterface.php', + 'Symfony\\Component\\Form\\ResolvedFormTypeInterface' => $vendorDir . '/symfony/form/ResolvedFormTypeInterface.php', + 'Symfony\\Component\\Form\\ReversedTransformer' => $vendorDir . '/symfony/form/ReversedTransformer.php', + 'Symfony\\Component\\Form\\SubmitButton' => $vendorDir . '/symfony/form/SubmitButton.php', + 'Symfony\\Component\\Form\\SubmitButtonBuilder' => $vendorDir . '/symfony/form/SubmitButtonBuilder.php', + 'Symfony\\Component\\Form\\SubmitButtonTypeInterface' => $vendorDir . '/symfony/form/SubmitButtonTypeInterface.php', + 'Symfony\\Component\\Form\\Test\\FormBuilderInterface' => $vendorDir . '/symfony/form/Test/FormBuilderInterface.php', + 'Symfony\\Component\\Form\\Test\\FormIntegrationTestCase' => $vendorDir . '/symfony/form/Test/FormIntegrationTestCase.php', + 'Symfony\\Component\\Form\\Test\\FormInterface' => $vendorDir . '/symfony/form/Test/FormInterface.php', + 'Symfony\\Component\\Form\\Test\\FormPerformanceTestCase' => $vendorDir . '/symfony/form/Test/FormPerformanceTestCase.php', + 'Symfony\\Component\\Form\\Test\\Traits\\RunTestTrait' => $vendorDir . '/symfony/form/Test/Traits/RunTestTrait.php', + 'Symfony\\Component\\Form\\Test\\Traits\\ValidatorExtensionTrait' => $vendorDir . '/symfony/form/Test/Traits/ValidatorExtensionTrait.php', + 'Symfony\\Component\\Form\\Test\\TypeTestCase' => $vendorDir . '/symfony/form/Test/TypeTestCase.php', + 'Symfony\\Component\\Form\\Util\\FormUtil' => $vendorDir . '/symfony/form/Util/FormUtil.php', + 'Symfony\\Component\\Form\\Util\\InheritDataAwareIterator' => $vendorDir . '/symfony/form/Util/InheritDataAwareIterator.php', + 'Symfony\\Component\\Form\\Util\\OptionsResolverWrapper' => $vendorDir . '/symfony/form/Util/OptionsResolverWrapper.php', + 'Symfony\\Component\\Form\\Util\\OrderedHashMap' => $vendorDir . '/symfony/form/Util/OrderedHashMap.php', + 'Symfony\\Component\\Form\\Util\\OrderedHashMapIterator' => $vendorDir . '/symfony/form/Util/OrderedHashMapIterator.php', + 'Symfony\\Component\\Form\\Util\\ServerParams' => $vendorDir . '/symfony/form/Util/ServerParams.php', + 'Symfony\\Component\\Form\\Util\\StringUtil' => $vendorDir . '/symfony/form/Util/StringUtil.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => $vendorDir . '/symfony/http-foundation/AcceptHeader.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => $vendorDir . '/symfony/http-foundation/AcceptHeaderItem.php', 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => $vendorDir . '/symfony/http-foundation/BinaryFileResponse.php', @@ -2572,6 +2813,63 @@ 'Symfony\\Component\\Mime\\Part\\SMimePart' => $vendorDir . '/symfony/mime/Part/SMimePart.php', 'Symfony\\Component\\Mime\\Part\\TextPart' => $vendorDir . '/symfony/mime/Part/TextPart.php', 'Symfony\\Component\\Mime\\RawMessage' => $vendorDir . '/symfony/mime/RawMessage.php', + 'Symfony\\Component\\OptionsResolver\\Debug\\OptionsResolverIntrospector' => $vendorDir . '/symfony/options-resolver/Debug/OptionsResolverIntrospector.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\AccessException' => $vendorDir . '/symfony/options-resolver/Exception/AccessException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/options-resolver/Exception/ExceptionInterface.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/options-resolver/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException' => $vendorDir . '/symfony/options-resolver/Exception/InvalidOptionsException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException' => $vendorDir . '/symfony/options-resolver/Exception/MissingOptionsException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\NoConfigurationException' => $vendorDir . '/symfony/options-resolver/Exception/NoConfigurationException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\NoSuchOptionException' => $vendorDir . '/symfony/options-resolver/Exception/NoSuchOptionException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\OptionDefinitionException' => $vendorDir . '/symfony/options-resolver/Exception/OptionDefinitionException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException' => $vendorDir . '/symfony/options-resolver/Exception/UndefinedOptionsException.php', + 'Symfony\\Component\\OptionsResolver\\OptionConfigurator' => $vendorDir . '/symfony/options-resolver/OptionConfigurator.php', + 'Symfony\\Component\\OptionsResolver\\Options' => $vendorDir . '/symfony/options-resolver/Options.php', + 'Symfony\\Component\\OptionsResolver\\OptionsResolver' => $vendorDir . '/symfony/options-resolver/OptionsResolver.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\AccessException' => $vendorDir . '/symfony/property-access/Exception/AccessException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/property-access/Exception/ExceptionInterface.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/property-access/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\InvalidPropertyPathException' => $vendorDir . '/symfony/property-access/Exception/InvalidPropertyPathException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\NoSuchIndexException' => $vendorDir . '/symfony/property-access/Exception/NoSuchIndexException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException' => $vendorDir . '/symfony/property-access/Exception/NoSuchPropertyException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\OutOfBoundsException' => $vendorDir . '/symfony/property-access/Exception/OutOfBoundsException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\RuntimeException' => $vendorDir . '/symfony/property-access/Exception/RuntimeException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\UnexpectedTypeException' => $vendorDir . '/symfony/property-access/Exception/UnexpectedTypeException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\UninitializedPropertyException' => $vendorDir . '/symfony/property-access/Exception/UninitializedPropertyException.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccess' => $vendorDir . '/symfony/property-access/PropertyAccess.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccessor' => $vendorDir . '/symfony/property-access/PropertyAccessor.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder' => $vendorDir . '/symfony/property-access/PropertyAccessorBuilder.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccessorInterface' => $vendorDir . '/symfony/property-access/PropertyAccessorInterface.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPath' => $vendorDir . '/symfony/property-access/PropertyPath.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathBuilder' => $vendorDir . '/symfony/property-access/PropertyPathBuilder.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathInterface' => $vendorDir . '/symfony/property-access/PropertyPathInterface.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathIterator' => $vendorDir . '/symfony/property-access/PropertyPathIterator.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathIteratorInterface' => $vendorDir . '/symfony/property-access/PropertyPathIteratorInterface.php', + 'Symfony\\Component\\PropertyInfo\\DependencyInjection\\PropertyInfoConstructorPass' => $vendorDir . '/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php', + 'Symfony\\Component\\PropertyInfo\\DependencyInjection\\PropertyInfoPass' => $vendorDir . '/symfony/property-info/DependencyInjection/PropertyInfoPass.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorArgumentTypeExtractorInterface' => $vendorDir . '/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorExtractor' => $vendorDir . '/symfony/property-info/Extractor/ConstructorExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor' => $vendorDir . '/symfony/property-info/Extractor/PhpDocExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor' => $vendorDir . '/symfony/property-info/Extractor/PhpStanExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor' => $vendorDir . '/symfony/property-info/Extractor/ReflectionExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor' => $vendorDir . '/symfony/property-info/Extractor/SerializerExtractor.php', + 'Symfony\\Component\\PropertyInfo\\PhpStan\\NameScope' => $vendorDir . '/symfony/property-info/PhpStan/NameScope.php', + 'Symfony\\Component\\PropertyInfo\\PhpStan\\NameScopeFactory' => $vendorDir . '/symfony/property-info/PhpStan/NameScopeFactory.php', + 'Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyAccessExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyDescriptionExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInfoCacheExtractor' => $vendorDir . '/symfony/property-info/PropertyInfoCacheExtractor.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor' => $vendorDir . '/symfony/property-info/PropertyInfoExtractor.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInfoExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyInfoExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyInitializableExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyListExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyReadInfo' => $vendorDir . '/symfony/property-info/PropertyReadInfo.php', + 'Symfony\\Component\\PropertyInfo\\PropertyReadInfoExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyReadInfoExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyTypeExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyWriteInfo' => $vendorDir . '/symfony/property-info/PropertyWriteInfo.php', + 'Symfony\\Component\\PropertyInfo\\PropertyWriteInfoExtractorInterface' => $vendorDir . '/symfony/property-info/PropertyWriteInfoExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\Type' => $vendorDir . '/symfony/property-info/Type.php', + 'Symfony\\Component\\PropertyInfo\\Util\\PhpDocTypeHelper' => $vendorDir . '/symfony/property-info/Util/PhpDocTypeHelper.php', + 'Symfony\\Component\\PropertyInfo\\Util\\PhpStanTypeHelper' => $vendorDir . '/symfony/property-info/Util/PhpStanTypeHelper.php', 'Symfony\\Component\\Routing\\Alias' => $vendorDir . '/symfony/routing/Alias.php', 'Symfony\\Component\\Routing\\Annotation\\Route' => $vendorDir . '/symfony/routing/Annotation/Route.php', 'Symfony\\Component\\Routing\\Attribute\\Route' => $vendorDir . '/symfony/routing/Attribute/Route.php', @@ -2796,6 +3094,35 @@ 'Symfony\\Contracts\\Translation\\TranslatorTrait' => $vendorDir . '/symfony/translation-contracts/TranslatorTrait.php', 'Symfony\\Polyfill\\Ctype\\Ctype' => $vendorDir . '/symfony/polyfill-ctype/Ctype.php', 'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => $vendorDir . '/symfony/polyfill-intl-grapheme/Grapheme.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Collator' => $vendorDir . '/symfony/polyfill-intl-icu/Collator.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Currencies' => $vendorDir . '/symfony/polyfill-intl-icu/Currencies.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\AmPmTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/AmPmTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\DayOfWeekTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/DayOfWeekTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\DayOfYearTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/DayOfYearTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\DayTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/DayTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\FullTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour1200Transformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/Hour1200Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour1201Transformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/Hour1201Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour2400Transformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/Hour2400Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour2401Transformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/Hour2401Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\HourTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/HourTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\MinuteTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/MinuteTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\MonthTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/MonthTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\QuarterTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/QuarterTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\SecondTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/SecondTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\TimezoneTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/TimezoneTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Transformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\YearTransformer' => $vendorDir . '/symfony/polyfill-intl-icu/DateFormat/YearTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/polyfill-intl-icu/Exception/ExceptionInterface.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\MethodArgumentNotImplementedException' => $vendorDir . '/symfony/polyfill-intl-icu/Exception/MethodArgumentNotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\MethodArgumentValueNotImplementedException' => $vendorDir . '/symfony/polyfill-intl-icu/Exception/MethodArgumentValueNotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\MethodNotImplementedException' => $vendorDir . '/symfony/polyfill-intl-icu/Exception/MethodNotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\NotImplementedException' => $vendorDir . '/symfony/polyfill-intl-icu/Exception/NotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\RuntimeException' => $vendorDir . '/symfony/polyfill-intl-icu/Exception/RuntimeException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Icu' => $vendorDir . '/symfony/polyfill-intl-icu/Icu.php', + 'Symfony\\Polyfill\\Intl\\Icu\\IntlDateFormatter' => $vendorDir . '/symfony/polyfill-intl-icu/IntlDateFormatter.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Locale' => $vendorDir . '/symfony/polyfill-intl-icu/Locale.php', + 'Symfony\\Polyfill\\Intl\\Icu\\NumberFormatter' => $vendorDir . '/symfony/polyfill-intl-icu/NumberFormatter.php', 'Symfony\\Polyfill\\Intl\\Idn\\Idn' => $vendorDir . '/symfony/polyfill-intl-idn/Idn.php', 'Symfony\\Polyfill\\Intl\\Idn\\Info' => $vendorDir . '/symfony/polyfill-intl-idn/Info.php', 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\DisallowedRanges' => $vendorDir . '/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php', diff --git a/lib/composer/autoload_files.php b/lib/composer/autoload_files.php index 982b4f47c4..b0244afb94 100644 --- a/lib/composer/autoload_files.php +++ b/lib/composer/autoload_files.php @@ -17,10 +17,11 @@ 'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php', 'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', - 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', - '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', + '6a47392539ca2329373e0d33e1dba053' => $vendorDir . '/symfony/polyfill-intl-icu/bootstrap.php', '344f11dc3484aaed5cbde58e23513be4' => $vendorDir . '/apereo/phpcas/source/CAS.php', '6997bc0ca52a383ea79e2a4a84bb1f3e' => $baseDir . '/sources/alias.php', ); diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 6816f3d267..83a2f05fe6 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -13,6 +13,7 @@ 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), + 'Symfony\\Polyfill\\Intl\\Icu\\' => array($vendorDir . '/symfony/polyfill-intl-icu'), 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), 'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'), @@ -26,10 +27,14 @@ 'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'), 'Symfony\\Component\\Runtime\\' => array($vendorDir . '/symfony/runtime'), 'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'), + 'Symfony\\Component\\PropertyInfo\\' => array($vendorDir . '/symfony/property-info'), + 'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'), + 'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'), 'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'), 'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'), 'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'), 'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'), + 'Symfony\\Component\\Form\\' => array($vendorDir . '/symfony/form'), 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'), 'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'), 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index e8dbf5f912..5e5356cf1c 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -18,10 +18,11 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php', 'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', - 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', - '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + '6a47392539ca2329373e0d33e1dba053' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/bootstrap.php', '344f11dc3484aaed5cbde58e23513be4' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS.php', '6997bc0ca52a383ea79e2a4a84bb1f3e' => __DIR__ . '/../..' . '/sources/alias.php', ); @@ -39,6 +40,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, 'Symfony\\Polyfill\\Intl\\Idn\\' => 26, + 'Symfony\\Polyfill\\Intl\\Icu\\' => 26, 'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31, 'Symfony\\Polyfill\\Ctype\\' => 23, 'Symfony\\Contracts\\Translation\\' => 30, @@ -52,10 +54,14 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\Stopwatch\\' => 28, 'Symfony\\Component\\Runtime\\' => 26, 'Symfony\\Component\\Routing\\' => 26, + 'Symfony\\Component\\PropertyInfo\\' => 31, + 'Symfony\\Component\\PropertyAccess\\' => 33, + 'Symfony\\Component\\OptionsResolver\\' => 34, 'Symfony\\Component\\Mime\\' => 23, 'Symfony\\Component\\Mailer\\' => 25, 'Symfony\\Component\\HttpKernel\\' => 29, 'Symfony\\Component\\HttpFoundation\\' => 33, + 'Symfony\\Component\\Form\\' => 23, 'Symfony\\Component\\Finder\\' => 25, 'Symfony\\Component\\Filesystem\\' => 29, 'Symfony\\Component\\EventDispatcher\\' => 34, @@ -139,6 +145,10 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', ), + 'Symfony\\Polyfill\\Intl\\Icu\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-icu', + ), 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', @@ -191,6 +201,18 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f array ( 0 => __DIR__ . '/..' . '/symfony/routing', ), + 'Symfony\\Component\\PropertyInfo\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/property-info', + ), + 'Symfony\\Component\\PropertyAccess\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/property-access', + ), + 'Symfony\\Component\\OptionsResolver\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/options-resolver', + ), 'Symfony\\Component\\Mime\\' => array ( 0 => __DIR__ . '/..' . '/symfony/mime', @@ -207,6 +229,10 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f array ( 0 => __DIR__ . '/..' . '/symfony/http-foundation', ), + 'Symfony\\Component\\Form\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/form', + ), 'Symfony\\Component\\Finder\\' => array ( 0 => __DIR__ . '/..' . '/symfony/finder', @@ -483,6 +509,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'CharConcatWSExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', 'CheckStopWatchThresholds' => __DIR__ . '/../..' . '/core/ormstopwatch.class.inc.php', 'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', + 'Collator' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php', 'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/Application/Branding.php', 'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php', 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php', @@ -498,6 +525,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Application\\Search\\CriterionParser' => __DIR__ . '/../..' . '/sources/Application/Search/criterionparser.class.inc.php', 'Combodo\\iTop\\Application\\Search\\SearchForm' => __DIR__ . '/../..' . '/sources/Application/Search/searchform.class.inc.php', 'Combodo\\iTop\\Application\\Status\\Status' => __DIR__ . '/../..' . '/sources/Application/Status/Status.php', + 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormController' => __DIR__ . '/../..' . '/sources/Application/Symfony/Poc/BaseForm/BaseFormController.php', + 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormType' => __DIR__ . '/../..' . '/sources/Application/Symfony/Poc/BaseForm/BaseFormType.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\Controller' => __DIR__ . '/../..' . '/sources/Application/TwigBase/Controller/Controller.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\PageNotFoundException' => __DIR__ . '/../..' . '/application/exceptions/PageNotFoundException.php', 'Combodo\\iTop\\Application\\TwigBase\\Twig\\Extension' => __DIR__ . '/../..' . '/sources/Application/TwigBase/Twig/Extension.php', @@ -1195,6 +1224,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'InputOutputTask' => __DIR__ . '/../..' . '/application/iotask.class.inc.php', 'IntervalExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', 'IntervalOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', + 'IntlDateFormatter' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/IntlDateFormatter.php', 'Introspection' => __DIR__ . '/../..' . '/core/introspection.class.inc.php', 'InvalidConfigParamException' => __DIR__ . '/../..' . '/application/exceptions/InvalidConfigParamException.php', 'InvalidExternalKeyValueException' => __DIR__ . '/../..' . '/application/exceptions/InvalidExternalKeyValueException.php', @@ -1236,6 +1266,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'League\\OAuth2\\Client\\Tool\\RequiredParameterTrait' => __DIR__ . '/..' . '/league/oauth2-client/src/Tool/RequiredParameterTrait.php', 'ListExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', 'ListOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', + 'Locale' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/Locale.php', 'LogAPI' => __DIR__ . '/../..' . '/core/log.class.inc.php', 'LogChannels' => __DIR__ . '/../..' . '/core/log.class.inc.php', 'LogFileNameBuilderFactory' => __DIR__ . '/../..' . '/core/log.class.inc.php', @@ -1269,6 +1300,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'NewsroomProviderBase' => __DIR__ . '/../..' . '/application/newsroomprovider.class.inc.php', 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'NotYetEvaluatedExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', + 'NumberFormatter' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/NumberFormatter.php', 'OQLActualClassTreeResolver' => __DIR__ . '/../..' . '/core/oqlactualclasstreeresolver.class.inc.php', 'OQLClassNode' => __DIR__ . '/../..' . '/core/oqlclassnode.class.inc.php', 'OQLClassTreeBuilder' => __DIR__ . '/../..' . '/core/oqlclasstreebuilder.class.inc.php', @@ -2547,6 +2579,241 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\Finder\\Iterator\\SortableIterator' => __DIR__ . '/..' . '/symfony/finder/Iterator/SortableIterator.php', 'Symfony\\Component\\Finder\\Iterator\\VcsIgnoredFilterIterator' => __DIR__ . '/..' . '/symfony/finder/Iterator/VcsIgnoredFilterIterator.php', 'Symfony\\Component\\Finder\\SplFileInfo' => __DIR__ . '/..' . '/symfony/finder/SplFileInfo.php', + 'Symfony\\Component\\Form\\AbstractExtension' => __DIR__ . '/..' . '/symfony/form/AbstractExtension.php', + 'Symfony\\Component\\Form\\AbstractRendererEngine' => __DIR__ . '/..' . '/symfony/form/AbstractRendererEngine.php', + 'Symfony\\Component\\Form\\AbstractType' => __DIR__ . '/..' . '/symfony/form/AbstractType.php', + 'Symfony\\Component\\Form\\AbstractTypeExtension' => __DIR__ . '/..' . '/symfony/form/AbstractTypeExtension.php', + 'Symfony\\Component\\Form\\Button' => __DIR__ . '/..' . '/symfony/form/Button.php', + 'Symfony\\Component\\Form\\ButtonBuilder' => __DIR__ . '/..' . '/symfony/form/ButtonBuilder.php', + 'Symfony\\Component\\Form\\ButtonTypeInterface' => __DIR__ . '/..' . '/symfony/form/ButtonTypeInterface.php', + 'Symfony\\Component\\Form\\CallbackTransformer' => __DIR__ . '/..' . '/symfony/form/CallbackTransformer.php', + 'Symfony\\Component\\Form\\ChoiceList\\ArrayChoiceList' => __DIR__ . '/..' . '/symfony/form/ChoiceList/ArrayChoiceList.php', + 'Symfony\\Component\\Form\\ChoiceList\\ChoiceList' => __DIR__ . '/..' . '/symfony/form/ChoiceList/ChoiceList.php', + 'Symfony\\Component\\Form\\ChoiceList\\ChoiceListInterface' => __DIR__ . '/..' . '/symfony/form/ChoiceList/ChoiceListInterface.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\AbstractStaticOption' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/AbstractStaticOption.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceAttr' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceAttr.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceFieldName' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceFieldName.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceFilter' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceFilter.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceLabel' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceLabel.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceLoader' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceTranslationParameters' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceTranslationParameters.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\ChoiceValue' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/ChoiceValue.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\GroupBy' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/GroupBy.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\Cache\\PreferredChoice' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/Cache/PreferredChoice.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\CachingFactoryDecorator' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/CachingFactoryDecorator.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\ChoiceListFactoryInterface' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/ChoiceListFactoryInterface.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\DefaultChoiceListFactory' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/DefaultChoiceListFactory.php', + 'Symfony\\Component\\Form\\ChoiceList\\Factory\\PropertyAccessDecorator' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Factory/PropertyAccessDecorator.php', + 'Symfony\\Component\\Form\\ChoiceList\\LazyChoiceList' => __DIR__ . '/..' . '/symfony/form/ChoiceList/LazyChoiceList.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Loader/AbstractChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\CallbackChoiceLoader' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Loader/CallbackChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Loader/ChoiceLoaderInterface.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\FilterChoiceLoaderDecorator' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Loader/FilterChoiceLoaderDecorator.php', + 'Symfony\\Component\\Form\\ChoiceList\\Loader\\IntlCallbackChoiceLoader' => __DIR__ . '/..' . '/symfony/form/ChoiceList/Loader/IntlCallbackChoiceLoader.php', + 'Symfony\\Component\\Form\\ChoiceList\\View\\ChoiceGroupView' => __DIR__ . '/..' . '/symfony/form/ChoiceList/View/ChoiceGroupView.php', + 'Symfony\\Component\\Form\\ChoiceList\\View\\ChoiceListView' => __DIR__ . '/..' . '/symfony/form/ChoiceList/View/ChoiceListView.php', + 'Symfony\\Component\\Form\\ChoiceList\\View\\ChoiceView' => __DIR__ . '/..' . '/symfony/form/ChoiceList/View/ChoiceView.php', + 'Symfony\\Component\\Form\\ClearableErrorsInterface' => __DIR__ . '/..' . '/symfony/form/ClearableErrorsInterface.php', + 'Symfony\\Component\\Form\\ClickableInterface' => __DIR__ . '/..' . '/symfony/form/ClickableInterface.php', + 'Symfony\\Component\\Form\\Command\\DebugCommand' => __DIR__ . '/..' . '/symfony/form/Command/DebugCommand.php', + 'Symfony\\Component\\Form\\Console\\Descriptor\\Descriptor' => __DIR__ . '/..' . '/symfony/form/Console/Descriptor/Descriptor.php', + 'Symfony\\Component\\Form\\Console\\Descriptor\\JsonDescriptor' => __DIR__ . '/..' . '/symfony/form/Console/Descriptor/JsonDescriptor.php', + 'Symfony\\Component\\Form\\Console\\Descriptor\\TextDescriptor' => __DIR__ . '/..' . '/symfony/form/Console/Descriptor/TextDescriptor.php', + 'Symfony\\Component\\Form\\Console\\Helper\\DescriptorHelper' => __DIR__ . '/..' . '/symfony/form/Console/Helper/DescriptorHelper.php', + 'Symfony\\Component\\Form\\DataAccessorInterface' => __DIR__ . '/..' . '/symfony/form/DataAccessorInterface.php', + 'Symfony\\Component\\Form\\DataMapperInterface' => __DIR__ . '/..' . '/symfony/form/DataMapperInterface.php', + 'Symfony\\Component\\Form\\DataTransformerInterface' => __DIR__ . '/..' . '/symfony/form/DataTransformerInterface.php', + 'Symfony\\Component\\Form\\DependencyInjection\\FormPass' => __DIR__ . '/..' . '/symfony/form/DependencyInjection/FormPass.php', + 'Symfony\\Component\\Form\\Event\\PostSetDataEvent' => __DIR__ . '/..' . '/symfony/form/Event/PostSetDataEvent.php', + 'Symfony\\Component\\Form\\Event\\PostSubmitEvent' => __DIR__ . '/..' . '/symfony/form/Event/PostSubmitEvent.php', + 'Symfony\\Component\\Form\\Event\\PreSetDataEvent' => __DIR__ . '/..' . '/symfony/form/Event/PreSetDataEvent.php', + 'Symfony\\Component\\Form\\Event\\PreSubmitEvent' => __DIR__ . '/..' . '/symfony/form/Event/PreSubmitEvent.php', + 'Symfony\\Component\\Form\\Event\\SubmitEvent' => __DIR__ . '/..' . '/symfony/form/Event/SubmitEvent.php', + 'Symfony\\Component\\Form\\Exception\\AccessException' => __DIR__ . '/..' . '/symfony/form/Exception/AccessException.php', + 'Symfony\\Component\\Form\\Exception\\AlreadySubmittedException' => __DIR__ . '/..' . '/symfony/form/Exception/AlreadySubmittedException.php', + 'Symfony\\Component\\Form\\Exception\\BadMethodCallException' => __DIR__ . '/..' . '/symfony/form/Exception/BadMethodCallException.php', + 'Symfony\\Component\\Form\\Exception\\ErrorMappingException' => __DIR__ . '/..' . '/symfony/form/Exception/ErrorMappingException.php', + 'Symfony\\Component\\Form\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/form/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Form\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/form/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Form\\Exception\\InvalidConfigurationException' => __DIR__ . '/..' . '/symfony/form/Exception/InvalidConfigurationException.php', + 'Symfony\\Component\\Form\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/form/Exception/LogicException.php', + 'Symfony\\Component\\Form\\Exception\\OutOfBoundsException' => __DIR__ . '/..' . '/symfony/form/Exception/OutOfBoundsException.php', + 'Symfony\\Component\\Form\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/form/Exception/RuntimeException.php', + 'Symfony\\Component\\Form\\Exception\\StringCastException' => __DIR__ . '/..' . '/symfony/form/Exception/StringCastException.php', + 'Symfony\\Component\\Form\\Exception\\TransformationFailedException' => __DIR__ . '/..' . '/symfony/form/Exception/TransformationFailedException.php', + 'Symfony\\Component\\Form\\Exception\\UnexpectedTypeException' => __DIR__ . '/..' . '/symfony/form/Exception/UnexpectedTypeException.php', + 'Symfony\\Component\\Form\\Extension\\Core\\CoreExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Core/CoreExtension.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataAccessor\\CallbackAccessor' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataAccessor/CallbackAccessor.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataAccessor\\ChainAccessor' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataAccessor\\PropertyPathAccessor' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataMapper\\CheckboxListMapper' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataMapper/CheckboxListMapper.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataMapper\\DataMapper' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataMapper/DataMapper.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataMapper\\RadioListMapper' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataMapper/RadioListMapper.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ArrayToPartsTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/ArrayToPartsTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\BaseDateTimeTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/BaseDateTimeTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\BooleanToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/BooleanToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ChoiceToValueTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ChoicesToValuesTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DataTransformerChain' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DataTransformerChain.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateIntervalToArrayTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateIntervalToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeImmutableToDateTimeTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToArrayTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToHtml5LocalDateTimeTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToLocalizedStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToRfc3339Transformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeToTimestampTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DateTimeZoneToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\IntegerToLocalizedStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\IntlTimeZoneToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\MoneyToLocalizedStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\NumberToLocalizedStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\PercentToLocalizedStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\StringToFloatTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/StringToFloatTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\UlidToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/UlidToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\UuidToStringTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/UuidToStringTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ValueToDuplicatesTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\WeekToArrayTransformer' => __DIR__ . '/..' . '/symfony/form/Extension/Core/DataTransformer/WeekToArrayTransformer.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\FixUrlProtocolListener' => __DIR__ . '/..' . '/symfony/form/Extension/Core/EventListener/FixUrlProtocolListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\MergeCollectionListener' => __DIR__ . '/..' . '/symfony/form/Extension/Core/EventListener/MergeCollectionListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\ResizeFormListener' => __DIR__ . '/..' . '/symfony/form/Extension/Core/EventListener/ResizeFormListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\TransformationFailureListener' => __DIR__ . '/..' . '/symfony/form/Extension/Core/EventListener/TransformationFailureListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\EventListener\\TrimListener' => __DIR__ . '/..' . '/symfony/form/Extension/Core/EventListener/TrimListener.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\BaseType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/BaseType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/BirthdayType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/ButtonType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/CheckboxType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/ChoiceType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/CollectionType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ColorType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/ColorType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CountryType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/CountryType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\CurrencyType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/CurrencyType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateIntervalType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/DateIntervalType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/DateTimeType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/DateType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/EmailType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\EnumType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/EnumType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/FileType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/FormType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/HiddenType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/IntegerType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\LanguageType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/LanguageType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\LocaleType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/LocaleType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\MoneyType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/MoneyType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/NumberType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/PasswordType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\PercentType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/PercentType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\RadioType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/RadioType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\RangeType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/RangeType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/RepeatedType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\ResetType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/ResetType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/SearchType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/SubmitType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TelType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/TelType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/TextType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/TextareaType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TimeType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/TimeType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TimezoneType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/TimezoneType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\TransformationFailureExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/TransformationFailureExtension.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UlidType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/UlidType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/UrlType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UuidType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/UuidType.php', + 'Symfony\\Component\\Form\\Extension\\Core\\Type\\WeekType' => __DIR__ . '/..' . '/symfony/form/Extension/Core/Type/WeekType.php', + 'Symfony\\Component\\Form\\Extension\\Csrf\\CsrfExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Csrf/CsrfExtension.php', + 'Symfony\\Component\\Form\\Extension\\Csrf\\EventListener\\CsrfValidationListener' => __DIR__ . '/..' . '/symfony/form/Extension/Csrf/EventListener/CsrfValidationListener.php', + 'Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\DataCollectorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/DataCollectorExtension.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\EventListener\\DataCollectorListener' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/EventListener/DataCollectorListener.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataCollector' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/FormDataCollector.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataCollectorInterface' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/FormDataCollectorInterface.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataExtractor' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/FormDataExtractor.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\FormDataExtractorInterface' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/FormDataExtractorInterface.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\Proxy\\ResolvedTypeDataCollectorProxy' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\Proxy\\ResolvedTypeFactoryDataCollectorProxy' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php', + 'Symfony\\Component\\Form\\Extension\\DataCollector\\Type\\DataCollectorTypeExtension' => __DIR__ . '/..' . '/symfony/form/Extension/DataCollector/Type/DataCollectorTypeExtension.php', + 'Symfony\\Component\\Form\\Extension\\DependencyInjection\\DependencyInjectionExtension' => __DIR__ . '/..' . '/symfony/form/Extension/DependencyInjection/DependencyInjectionExtension.php', + 'Symfony\\Component\\Form\\Extension\\HtmlSanitizer\\HtmlSanitizerExtension' => __DIR__ . '/..' . '/symfony/form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php', + 'Symfony\\Component\\Form\\Extension\\HtmlSanitizer\\Type\\TextTypeHtmlSanitizerExtension' => __DIR__ . '/..' . '/symfony/form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php', + 'Symfony\\Component\\Form\\Extension\\HttpFoundation\\HttpFoundationExtension' => __DIR__ . '/..' . '/symfony/form/Extension/HttpFoundation/HttpFoundationExtension.php', + 'Symfony\\Component\\Form\\Extension\\HttpFoundation\\HttpFoundationRequestHandler' => __DIR__ . '/..' . '/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php', + 'Symfony\\Component\\Form\\Extension\\HttpFoundation\\Type\\FormTypeHttpFoundationExtension' => __DIR__ . '/..' . '/symfony/form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\EventListener\\PasswordHasherListener' => __DIR__ . '/..' . '/symfony/form/Extension/PasswordHasher/EventListener/PasswordHasherListener.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\PasswordHasherExtension' => __DIR__ . '/..' . '/symfony/form/Extension/PasswordHasher/PasswordHasherExtension.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\Type\\FormTypePasswordHasherExtension' => __DIR__ . '/..' . '/symfony/form/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php', + 'Symfony\\Component\\Form\\Extension\\PasswordHasher\\Type\\PasswordTypePasswordHasherExtension' => __DIR__ . '/..' . '/symfony/form/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Constraints\\Form' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Constraints/Form.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Constraints\\FormValidator' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Constraints/FormValidator.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\EventListener\\ValidationListener' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/EventListener/ValidationListener.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\BaseValidatorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Type/BaseValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\FormTypeValidatorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Type/FormTypeValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\RepeatedTypeValidatorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\SubmitTypeValidatorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Type/SubmitTypeValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\Type\\UploadValidatorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/Type/UploadValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ValidatorExtension' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ValidatorExtension.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ValidatorTypeGuesser' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ValidatorTypeGuesser.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\MappingRule' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ViolationMapper/MappingRule.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\RelativePath' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ViolationMapper/RelativePath.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationMapper' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ViolationMapper/ViolationMapper.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationMapperInterface' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationPath' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ViolationMapper/ViolationPath.php', + 'Symfony\\Component\\Form\\Extension\\Validator\\ViolationMapper\\ViolationPathIterator' => __DIR__ . '/..' . '/symfony/form/Extension/Validator/ViolationMapper/ViolationPathIterator.php', + 'Symfony\\Component\\Form\\FileUploadError' => __DIR__ . '/..' . '/symfony/form/FileUploadError.php', + 'Symfony\\Component\\Form\\Form' => __DIR__ . '/..' . '/symfony/form/Form.php', + 'Symfony\\Component\\Form\\FormBuilder' => __DIR__ . '/..' . '/symfony/form/FormBuilder.php', + 'Symfony\\Component\\Form\\FormBuilderInterface' => __DIR__ . '/..' . '/symfony/form/FormBuilderInterface.php', + 'Symfony\\Component\\Form\\FormConfigBuilder' => __DIR__ . '/..' . '/symfony/form/FormConfigBuilder.php', + 'Symfony\\Component\\Form\\FormConfigBuilderInterface' => __DIR__ . '/..' . '/symfony/form/FormConfigBuilderInterface.php', + 'Symfony\\Component\\Form\\FormConfigInterface' => __DIR__ . '/..' . '/symfony/form/FormConfigInterface.php', + 'Symfony\\Component\\Form\\FormError' => __DIR__ . '/..' . '/symfony/form/FormError.php', + 'Symfony\\Component\\Form\\FormErrorIterator' => __DIR__ . '/..' . '/symfony/form/FormErrorIterator.php', + 'Symfony\\Component\\Form\\FormEvent' => __DIR__ . '/..' . '/symfony/form/FormEvent.php', + 'Symfony\\Component\\Form\\FormEvents' => __DIR__ . '/..' . '/symfony/form/FormEvents.php', + 'Symfony\\Component\\Form\\FormExtensionInterface' => __DIR__ . '/..' . '/symfony/form/FormExtensionInterface.php', + 'Symfony\\Component\\Form\\FormFactory' => __DIR__ . '/..' . '/symfony/form/FormFactory.php', + 'Symfony\\Component\\Form\\FormFactoryBuilder' => __DIR__ . '/..' . '/symfony/form/FormFactoryBuilder.php', + 'Symfony\\Component\\Form\\FormFactoryBuilderInterface' => __DIR__ . '/..' . '/symfony/form/FormFactoryBuilderInterface.php', + 'Symfony\\Component\\Form\\FormFactoryInterface' => __DIR__ . '/..' . '/symfony/form/FormFactoryInterface.php', + 'Symfony\\Component\\Form\\FormInterface' => __DIR__ . '/..' . '/symfony/form/FormInterface.php', + 'Symfony\\Component\\Form\\FormRegistry' => __DIR__ . '/..' . '/symfony/form/FormRegistry.php', + 'Symfony\\Component\\Form\\FormRegistryInterface' => __DIR__ . '/..' . '/symfony/form/FormRegistryInterface.php', + 'Symfony\\Component\\Form\\FormRenderer' => __DIR__ . '/..' . '/symfony/form/FormRenderer.php', + 'Symfony\\Component\\Form\\FormRendererEngineInterface' => __DIR__ . '/..' . '/symfony/form/FormRendererEngineInterface.php', + 'Symfony\\Component\\Form\\FormRendererInterface' => __DIR__ . '/..' . '/symfony/form/FormRendererInterface.php', + 'Symfony\\Component\\Form\\FormTypeExtensionInterface' => __DIR__ . '/..' . '/symfony/form/FormTypeExtensionInterface.php', + 'Symfony\\Component\\Form\\FormTypeGuesserChain' => __DIR__ . '/..' . '/symfony/form/FormTypeGuesserChain.php', + 'Symfony\\Component\\Form\\FormTypeGuesserInterface' => __DIR__ . '/..' . '/symfony/form/FormTypeGuesserInterface.php', + 'Symfony\\Component\\Form\\FormTypeInterface' => __DIR__ . '/..' . '/symfony/form/FormTypeInterface.php', + 'Symfony\\Component\\Form\\FormView' => __DIR__ . '/..' . '/symfony/form/FormView.php', + 'Symfony\\Component\\Form\\Forms' => __DIR__ . '/..' . '/symfony/form/Forms.php', + 'Symfony\\Component\\Form\\Guess\\Guess' => __DIR__ . '/..' . '/symfony/form/Guess/Guess.php', + 'Symfony\\Component\\Form\\Guess\\TypeGuess' => __DIR__ . '/..' . '/symfony/form/Guess/TypeGuess.php', + 'Symfony\\Component\\Form\\Guess\\ValueGuess' => __DIR__ . '/..' . '/symfony/form/Guess/ValueGuess.php', + 'Symfony\\Component\\Form\\NativeRequestHandler' => __DIR__ . '/..' . '/symfony/form/NativeRequestHandler.php', + 'Symfony\\Component\\Form\\PreloadedExtension' => __DIR__ . '/..' . '/symfony/form/PreloadedExtension.php', + 'Symfony\\Component\\Form\\RequestHandlerInterface' => __DIR__ . '/..' . '/symfony/form/RequestHandlerInterface.php', + 'Symfony\\Component\\Form\\ResolvedFormType' => __DIR__ . '/..' . '/symfony/form/ResolvedFormType.php', + 'Symfony\\Component\\Form\\ResolvedFormTypeFactory' => __DIR__ . '/..' . '/symfony/form/ResolvedFormTypeFactory.php', + 'Symfony\\Component\\Form\\ResolvedFormTypeFactoryInterface' => __DIR__ . '/..' . '/symfony/form/ResolvedFormTypeFactoryInterface.php', + 'Symfony\\Component\\Form\\ResolvedFormTypeInterface' => __DIR__ . '/..' . '/symfony/form/ResolvedFormTypeInterface.php', + 'Symfony\\Component\\Form\\ReversedTransformer' => __DIR__ . '/..' . '/symfony/form/ReversedTransformer.php', + 'Symfony\\Component\\Form\\SubmitButton' => __DIR__ . '/..' . '/symfony/form/SubmitButton.php', + 'Symfony\\Component\\Form\\SubmitButtonBuilder' => __DIR__ . '/..' . '/symfony/form/SubmitButtonBuilder.php', + 'Symfony\\Component\\Form\\SubmitButtonTypeInterface' => __DIR__ . '/..' . '/symfony/form/SubmitButtonTypeInterface.php', + 'Symfony\\Component\\Form\\Test\\FormBuilderInterface' => __DIR__ . '/..' . '/symfony/form/Test/FormBuilderInterface.php', + 'Symfony\\Component\\Form\\Test\\FormIntegrationTestCase' => __DIR__ . '/..' . '/symfony/form/Test/FormIntegrationTestCase.php', + 'Symfony\\Component\\Form\\Test\\FormInterface' => __DIR__ . '/..' . '/symfony/form/Test/FormInterface.php', + 'Symfony\\Component\\Form\\Test\\FormPerformanceTestCase' => __DIR__ . '/..' . '/symfony/form/Test/FormPerformanceTestCase.php', + 'Symfony\\Component\\Form\\Test\\Traits\\RunTestTrait' => __DIR__ . '/..' . '/symfony/form/Test/Traits/RunTestTrait.php', + 'Symfony\\Component\\Form\\Test\\Traits\\ValidatorExtensionTrait' => __DIR__ . '/..' . '/symfony/form/Test/Traits/ValidatorExtensionTrait.php', + 'Symfony\\Component\\Form\\Test\\TypeTestCase' => __DIR__ . '/..' . '/symfony/form/Test/TypeTestCase.php', + 'Symfony\\Component\\Form\\Util\\FormUtil' => __DIR__ . '/..' . '/symfony/form/Util/FormUtil.php', + 'Symfony\\Component\\Form\\Util\\InheritDataAwareIterator' => __DIR__ . '/..' . '/symfony/form/Util/InheritDataAwareIterator.php', + 'Symfony\\Component\\Form\\Util\\OptionsResolverWrapper' => __DIR__ . '/..' . '/symfony/form/Util/OptionsResolverWrapper.php', + 'Symfony\\Component\\Form\\Util\\OrderedHashMap' => __DIR__ . '/..' . '/symfony/form/Util/OrderedHashMap.php', + 'Symfony\\Component\\Form\\Util\\OrderedHashMapIterator' => __DIR__ . '/..' . '/symfony/form/Util/OrderedHashMapIterator.php', + 'Symfony\\Component\\Form\\Util\\ServerParams' => __DIR__ . '/..' . '/symfony/form/Util/ServerParams.php', + 'Symfony\\Component\\Form\\Util\\StringUtil' => __DIR__ . '/..' . '/symfony/form/Util/StringUtil.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeader.php', 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeaderItem.php', 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => __DIR__ . '/..' . '/symfony/http-foundation/BinaryFileResponse.php', @@ -2927,6 +3194,63 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\Mime\\Part\\SMimePart' => __DIR__ . '/..' . '/symfony/mime/Part/SMimePart.php', 'Symfony\\Component\\Mime\\Part\\TextPart' => __DIR__ . '/..' . '/symfony/mime/Part/TextPart.php', 'Symfony\\Component\\Mime\\RawMessage' => __DIR__ . '/..' . '/symfony/mime/RawMessage.php', + 'Symfony\\Component\\OptionsResolver\\Debug\\OptionsResolverIntrospector' => __DIR__ . '/..' . '/symfony/options-resolver/Debug/OptionsResolverIntrospector.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\AccessException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/AccessException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/ExceptionInterface.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/InvalidOptionsException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/MissingOptionsException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\NoConfigurationException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/NoConfigurationException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\NoSuchOptionException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/NoSuchOptionException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\OptionDefinitionException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/OptionDefinitionException.php', + 'Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException' => __DIR__ . '/..' . '/symfony/options-resolver/Exception/UndefinedOptionsException.php', + 'Symfony\\Component\\OptionsResolver\\OptionConfigurator' => __DIR__ . '/..' . '/symfony/options-resolver/OptionConfigurator.php', + 'Symfony\\Component\\OptionsResolver\\Options' => __DIR__ . '/..' . '/symfony/options-resolver/Options.php', + 'Symfony\\Component\\OptionsResolver\\OptionsResolver' => __DIR__ . '/..' . '/symfony/options-resolver/OptionsResolver.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\AccessException' => __DIR__ . '/..' . '/symfony/property-access/Exception/AccessException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/property-access/Exception/ExceptionInterface.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/property-access/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\InvalidPropertyPathException' => __DIR__ . '/..' . '/symfony/property-access/Exception/InvalidPropertyPathException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\NoSuchIndexException' => __DIR__ . '/..' . '/symfony/property-access/Exception/NoSuchIndexException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException' => __DIR__ . '/..' . '/symfony/property-access/Exception/NoSuchPropertyException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\OutOfBoundsException' => __DIR__ . '/..' . '/symfony/property-access/Exception/OutOfBoundsException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/property-access/Exception/RuntimeException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\UnexpectedTypeException' => __DIR__ . '/..' . '/symfony/property-access/Exception/UnexpectedTypeException.php', + 'Symfony\\Component\\PropertyAccess\\Exception\\UninitializedPropertyException' => __DIR__ . '/..' . '/symfony/property-access/Exception/UninitializedPropertyException.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccess' => __DIR__ . '/..' . '/symfony/property-access/PropertyAccess.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccessor' => __DIR__ . '/..' . '/symfony/property-access/PropertyAccessor.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder' => __DIR__ . '/..' . '/symfony/property-access/PropertyAccessorBuilder.php', + 'Symfony\\Component\\PropertyAccess\\PropertyAccessorInterface' => __DIR__ . '/..' . '/symfony/property-access/PropertyAccessorInterface.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPath' => __DIR__ . '/..' . '/symfony/property-access/PropertyPath.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathBuilder' => __DIR__ . '/..' . '/symfony/property-access/PropertyPathBuilder.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathInterface' => __DIR__ . '/..' . '/symfony/property-access/PropertyPathInterface.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathIterator' => __DIR__ . '/..' . '/symfony/property-access/PropertyPathIterator.php', + 'Symfony\\Component\\PropertyAccess\\PropertyPathIteratorInterface' => __DIR__ . '/..' . '/symfony/property-access/PropertyPathIteratorInterface.php', + 'Symfony\\Component\\PropertyInfo\\DependencyInjection\\PropertyInfoConstructorPass' => __DIR__ . '/..' . '/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php', + 'Symfony\\Component\\PropertyInfo\\DependencyInjection\\PropertyInfoPass' => __DIR__ . '/..' . '/symfony/property-info/DependencyInjection/PropertyInfoPass.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorArgumentTypeExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorExtractor' => __DIR__ . '/..' . '/symfony/property-info/Extractor/ConstructorExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor' => __DIR__ . '/..' . '/symfony/property-info/Extractor/PhpDocExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor' => __DIR__ . '/..' . '/symfony/property-info/Extractor/PhpStanExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor' => __DIR__ . '/..' . '/symfony/property-info/Extractor/ReflectionExtractor.php', + 'Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor' => __DIR__ . '/..' . '/symfony/property-info/Extractor/SerializerExtractor.php', + 'Symfony\\Component\\PropertyInfo\\PhpStan\\NameScope' => __DIR__ . '/..' . '/symfony/property-info/PhpStan/NameScope.php', + 'Symfony\\Component\\PropertyInfo\\PhpStan\\NameScopeFactory' => __DIR__ . '/..' . '/symfony/property-info/PhpStan/NameScopeFactory.php', + 'Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyAccessExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyDescriptionExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInfoCacheExtractor' => __DIR__ . '/..' . '/symfony/property-info/PropertyInfoCacheExtractor.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor' => __DIR__ . '/..' . '/symfony/property-info/PropertyInfoExtractor.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInfoExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyInfoExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyInitializableExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyListExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyReadInfo' => __DIR__ . '/..' . '/symfony/property-info/PropertyReadInfo.php', + 'Symfony\\Component\\PropertyInfo\\PropertyReadInfoExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyReadInfoExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyTypeExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\PropertyWriteInfo' => __DIR__ . '/..' . '/symfony/property-info/PropertyWriteInfo.php', + 'Symfony\\Component\\PropertyInfo\\PropertyWriteInfoExtractorInterface' => __DIR__ . '/..' . '/symfony/property-info/PropertyWriteInfoExtractorInterface.php', + 'Symfony\\Component\\PropertyInfo\\Type' => __DIR__ . '/..' . '/symfony/property-info/Type.php', + 'Symfony\\Component\\PropertyInfo\\Util\\PhpDocTypeHelper' => __DIR__ . '/..' . '/symfony/property-info/Util/PhpDocTypeHelper.php', + 'Symfony\\Component\\PropertyInfo\\Util\\PhpStanTypeHelper' => __DIR__ . '/..' . '/symfony/property-info/Util/PhpStanTypeHelper.php', 'Symfony\\Component\\Routing\\Alias' => __DIR__ . '/..' . '/symfony/routing/Alias.php', 'Symfony\\Component\\Routing\\Annotation\\Route' => __DIR__ . '/..' . '/symfony/routing/Annotation/Route.php', 'Symfony\\Component\\Routing\\Attribute\\Route' => __DIR__ . '/..' . '/symfony/routing/Attribute/Route.php', @@ -3151,6 +3475,35 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Contracts\\Translation\\TranslatorTrait' => __DIR__ . '/..' . '/symfony/translation-contracts/TranslatorTrait.php', 'Symfony\\Polyfill\\Ctype\\Ctype' => __DIR__ . '/..' . '/symfony/polyfill-ctype/Ctype.php', 'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/Grapheme.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Collator' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Collator.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Currencies' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Currencies.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\AmPmTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/AmPmTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\DayOfWeekTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/DayOfWeekTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\DayOfYearTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/DayOfYearTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\DayTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/DayTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\FullTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour1200Transformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/Hour1200Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour1201Transformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/Hour1201Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour2400Transformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/Hour2400Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Hour2401Transformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/Hour2401Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\HourTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/HourTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\MinuteTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/MinuteTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\MonthTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/MonthTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\QuarterTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/QuarterTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\SecondTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/SecondTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\TimezoneTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/TimezoneTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\Transformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/Transformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\DateFormat\\YearTransformer' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/DateFormat/YearTransformer.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Exception/ExceptionInterface.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\MethodArgumentNotImplementedException' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Exception/MethodArgumentNotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\MethodArgumentValueNotImplementedException' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Exception/MethodArgumentValueNotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\MethodNotImplementedException' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Exception/MethodNotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\NotImplementedException' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Exception/NotImplementedException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Exception/RuntimeException.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Icu' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Icu.php', + 'Symfony\\Polyfill\\Intl\\Icu\\IntlDateFormatter' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/IntlDateFormatter.php', + 'Symfony\\Polyfill\\Intl\\Icu\\Locale' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Locale.php', + 'Symfony\\Polyfill\\Intl\\Icu\\NumberFormatter' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/NumberFormatter.php', 'Symfony\\Polyfill\\Intl\\Idn\\Idn' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Idn.php', 'Symfony\\Polyfill\\Intl\\Idn\\Info' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Info.php', 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\DisallowedRanges' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php', diff --git a/lib/composer/installed.json b/lib/composer/installed.json index 681a0acf73..ff4ef935ef 100644 --- a/lib/composer/installed.json +++ b/lib/composer/installed.json @@ -2936,6 +2936,110 @@ ], "install-path": "../symfony/finder" }, + { + "name": "symfony/form", + "version": "v6.4.26", + "version_normalized": "6.4.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "b40cdbe70be9274ea807ef61da7d0f8d1c70dc51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/b40cdbe70be9274ea807ef61da7d0f8d1c70dc51", + "reference": "b40cdbe70be9274ea807ef61da7d0f8d1c70dc51", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", + "symfony/error-handler": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.3" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.2|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "time": "2025-09-20T07:40:41+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/form/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/form" + }, { "name": "symfony/framework-bundle", "version": "v6.4.25", @@ -3476,6 +3580,80 @@ ], "install-path": "../symfony/mime" }, + { + "name": "symfony/options-resolver", + "version": "v6.4.25", + "version_normalized": "6.4.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d28e7e2db8a73e9511df892d36445f61314bbebe", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "time": "2025-08-04T17:06:28+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/options-resolver" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -3647,6 +3825,97 @@ ], "install-path": "../symfony/polyfill-intl-grapheme" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.33.0", + "version_normalized": "1.33.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "time": "2025-06-20T22:24:30+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-icu" + }, { "name": "symfony/polyfill-intl-idn", "version": "v1.33.0", @@ -3996,6 +4265,183 @@ ], "install-path": "../symfony/polyfill-php83" }, + { + "name": "symfony/property-access", + "version": "v6.4.25", + "version_normalized": "6.4.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "fedc771326d4978a7d3167fa009a509b06a2e168" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/fedc771326d4978a7d3167fa009a509b06a2e168", + "reference": "fedc771326d4978a7d3167fa009a509b06a2e168", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/property-info": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0|^7.0" + }, + "time": "2025-08-12T15:42:57+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/property-access" + }, + { + "name": "symfony/property-info", + "version": "v6.4.24", + "version_normalized": "6.4.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "1056ae3621eeddd78d7c5ec074f1c1784324eec6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/1056ae3621eeddd78d7c5ec074f1c1784324eec6", + "reference": "1056ae3621eeddd78d7c5ec074f1c1784324eec6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0" + }, + "time": "2025-07-14T16:38:25+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/property-info" + }, { "name": "symfony/routing", "version": "v6.4.24", diff --git a/lib/composer/installed.php b/lib/composer/installed.php index a513414cd9..5010615635 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '7e515e7216f019f4c69e3699ad9bc6221988ff1e', + 'reference' => '8f038d2f9529e3d1ffbb08b0dc05931e958f28fc', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '7e515e7216f019f4c69e3699ad9bc6221988ff1e', + 'reference' => '8f038d2f9529e3d1ffbb08b0dc05931e958f28fc', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -447,6 +447,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/form' => array( + 'pretty_version' => 'v6.4.26', + 'version' => '6.4.26.0', + 'reference' => 'b40cdbe70be9274ea807ef61da7d0f8d1c70dc51', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/form', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/framework-bundle' => array( 'pretty_version' => 'v6.4.25', 'version' => '6.4.25.0', @@ -492,6 +501,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/options-resolver' => array( + 'pretty_version' => 'v6.4.25', + 'version' => '6.4.25.0', + 'reference' => 'd28e7e2db8a73e9511df892d36445f61314bbebe', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/options-resolver', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/polyfill-ctype' => array( 'pretty_version' => 'v1.33.0', 'version' => '1.33.0.0', @@ -510,6 +528,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/polyfill-intl-icu' => array( + 'pretty_version' => 'v1.33.0', + 'version' => '1.33.0.0', + 'reference' => 'bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-icu', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/polyfill-intl-idn' => array( 'pretty_version' => 'v1.33.0', 'version' => '1.33.0.0', @@ -546,6 +573,24 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/property-access' => array( + 'pretty_version' => 'v6.4.25', + 'version' => '6.4.25.0', + 'reference' => 'fedc771326d4978a7d3167fa009a509b06a2e168', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/property-access', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/property-info' => array( + 'pretty_version' => 'v6.4.24', + 'version' => '6.4.24.0', + 'reference' => '1056ae3621eeddd78d7c5ec074f1c1784324eec6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/property-info', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/routing' => array( 'pretty_version' => 'v6.4.24', 'version' => '6.4.24.0', diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index 955a061fb0..4d7c8a6008 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -19,27 +19,34 @@ namespace Combodo\iTop\Application\TwigBase\Controller; -use Combodo\iTop\Application\WebPage\AjaxPage; use ApplicationMenu; use Combodo\iTop\Application\TwigBase\Twig\TwigHelper; +use Combodo\iTop\Application\WebPage\AjaxPage; +use Combodo\iTop\Application\WebPage\ErrorPage; +use Combodo\iTop\Application\WebPage\iTopWebPage; +use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\Controller\AbstractController; use Dict; -use Combodo\iTop\Application\WebPage\ErrorPage; use Exception; use ExecutionKPI; use IssueLog; -use Combodo\iTop\Application\WebPage\iTopWebPage; use LoginWebPage; use MetaModel; use ReflectionClass; use SetupPage; use SetupUtils; +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\HttpFoundation\Request; use Twig\Error\Error; use Twig\Error\SyntaxError; +use Twig\RuntimeLoader\FactoryRuntimeLoader; use utils; -use Combodo\iTop\Application\WebPage\WebPage; use ZipArchive; +//use Combodo\iTop\Forms\Forms; + abstract class Controller extends AbstractController { const ENUM_PAGE_TYPE_HTML = 'html'; @@ -80,6 +87,8 @@ abstract class Controller extends AbstractController private $m_bIsBreadCrumbEnabled = true; /** @var array contains same parameters as {@see iTopWebPage::SetBreadCrumbEntry()} */ private $m_aBreadCrumbEntry = []; + /** @var \Symfony\Component\HttpFoundation\Request */ + private Request $oRequest; /** * Controller constructor. @@ -89,6 +98,7 @@ abstract class Controller extends AbstractController */ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = []) { + $this->oRequest = Request::createFromGlobals(); $this->m_aLinkedScripts = []; $this->m_aLinkedStylesheets = []; $this->m_aSaas = []; @@ -96,6 +106,8 @@ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditional $this->m_aDefaultParams = []; $this->m_aBlockParams = []; $this->SetModuleName($sModuleName); + $aAdditionalPaths[] = APPROOT.'lib/symfony/twig-bridge/Resources/views/Form'; + $aAdditionalPaths[] = APPROOT.'templates'; if (strlen($sViewPath) > 0) { $this->SetViewPath($sViewPath, $aAdditionalPaths); if ($sModuleName != 'core') { @@ -135,6 +147,14 @@ public function InitFromModule() public function SetViewPath($sViewPath, $aAdditionalPaths = []) { $oTwig = TwigHelper::GetTwigEnvironment($sViewPath, $aAdditionalPaths); + $formEngine = new TwigRendererEngine(['application/forms/itop_console_layout.twig'], $oTwig); + $oTwig->addRuntimeLoader(new FactoryRuntimeLoader([ + FormRenderer::class => function () use ($formEngine): FormRenderer { + return new FormRenderer($formEngine, null); + }, + ])); + $oExt = new FormExtension(); + $oTwig->addExtension($oExt); $this->m_oTwig = $oTwig; } @@ -659,6 +679,24 @@ public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sI $this->m_aBreadCrumbEntry = [$sId, $sLabel, $sDescription, $sUrl, $sIcon]; } + public function GetRequest(): Request + { + return $this->oRequest; + } + +// public function GetFormBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface +// { +// return Forms::createFormFactory()->createBuilder($type, $data,$options); +// } +// +// public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface +// { +// if (is_null($data)) { +// $data = $type::GetDefaultData(); +// } +// return $this->GetFormBuilder($type, $data,$options)->getForm(); +// } + /** * @param $aParams * @param $sName diff --git a/templates/application/forms/itop_base_layout.html.twig b/templates/application/forms/itop_base_layout.html.twig new file mode 100644 index 0000000000..123de44ce4 --- /dev/null +++ b/templates/application/forms/itop_base_layout.html.twig @@ -0,0 +1,4 @@ +{% use "form_div_layout.html.twig" %} + +{# Widgets #} + diff --git a/templates/application/forms/itop_console_layout.twig b/templates/application/forms/itop_console_layout.twig new file mode 100644 index 0000000000..610fa26e3f --- /dev/null +++ b/templates/application/forms/itop_console_layout.twig @@ -0,0 +1,23 @@ +{% use "application/form/itop_base_layout.html.twig" %} + +{# Widgets #} + +{%- block widget_attributes -%} + {% if type == 'text' %}{% set ibo_class='ibo-input-string' %}{% else %}{% set ibo_class='ibo-input-' ~ type %}{% endif %} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' ibo-input ' ~ ibo_class)|trim}) %} + {{- parent() -}} +{%- endblock widget_attributes -%} + +{%- block form_label -%} + {%- if compound is defined and compound -%} + {%- set element = 'legend' -%} + {%- else -%} + {% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ibo-field--label')|trim}) %} + {%- endif -%} + {{- parent() -}} +{%- endblock form_label -%} + +{%- block form_row -%} + {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' ibo-field ibo-content-block ibo-block ibo-field-small')|trim}) %} + {{- parent() -}} +{%- endblock form_row -%} From 2ee704563bc79a336c0bd01c31f68d42d3ad9783 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Wed, 8 Oct 2025 14:10:18 +0200 Subject: [PATCH 02/10] wip --- .../TwigBase/Controller/Controller.php | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index 4d7c8a6008..e4e7945675 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -37,7 +37,11 @@ use SetupUtils; use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\Forms; use Symfony\Component\HttpFoundation\Request; use Twig\Error\Error; use Twig\Error\SyntaxError; @@ -684,18 +688,18 @@ public function GetRequest(): Request return $this->oRequest; } -// public function GetFormBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface -// { -// return Forms::createFormFactory()->createBuilder($type, $data,$options); -// } -// -// public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface -// { -// if (is_null($data)) { -// $data = $type::GetDefaultData(); -// } -// return $this->GetFormBuilder($type, $data,$options)->getForm(); -// } + public function GetFormBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface + { + return Forms::createFormFactory()->createBuilder($type, $data,$options); + } + + public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface + { + if (is_null($data)) { + $data = $type::GetDefaultData(); + } + return $this->GetFormBuilder($type, $data,$options)->getForm(); + } /** * @param $aParams From 58b47824cad73d157fde4bbb319711551e0e0869 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Wed, 8 Oct 2025 14:48:03 +0200 Subject: [PATCH 03/10] Add iTop Forms --- lib/composer/autoload_classmap.php | 1 + lib/composer/autoload_static.php | 1 + .../TwigBase/Controller/Controller.php | 2 +- sources/Forms/Forms.php | 42 +++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 sources/Forms/Forms.php diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 026659f548..fa3f3fbd94 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -473,6 +473,7 @@ 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => $baseDir . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => $baseDir . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => $baseDir . '/sources/Form/Validator/SelectObjectValidator.php', + 'Combodo\\iTop\\Forms\\Forms' => $baseDir . '/sources/Forms/Forms.php', 'Combodo\\iTop\\Kernel' => $baseDir . '/sources/Kernel.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 5e5356cf1c..1b5fc85649 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -854,6 +854,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/SelectObjectValidator.php', + 'Combodo\\iTop\\Forms\\Forms' => __DIR__ . '/../..' . '/sources/Forms/Forms.php', 'Combodo\\iTop\\Kernel' => __DIR__ . '/../..' . '/sources/Kernel.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php', diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index e4e7945675..7bc23b44e2 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -26,6 +26,7 @@ use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\Controller\AbstractController; +use Combodo\iTop\Forms\Forms; use Dict; use Exception; use ExecutionKPI; @@ -41,7 +42,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRenderer; -use Symfony\Component\Form\Forms; use Symfony\Component\HttpFoundation\Request; use Twig\Error\Error; use Twig\Error\SyntaxError; diff --git a/sources/Forms/Forms.php b/sources/Forms/Forms.php new file mode 100644 index 0000000000..8dab5bc770 --- /dev/null +++ b/sources/Forms/Forms.php @@ -0,0 +1,42 @@ +getFormFactory(); + } + + /** + * Creates a form factory builder with the iTop configuration. + */ + public static function createFormFactoryBuilder(): FormFactoryBuilderInterface + { + return (new FormFactoryBuilder()) + ->addExtension(new HttpFoundationExtension()); + } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } +} \ No newline at end of file From c07c3cdca1e8472cb54809021b0856d8682455c6 Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Thu, 9 Oct 2025 07:52:55 +0200 Subject: [PATCH 04/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20twig=20base=20controller=20w?= =?UTF-8?q?ith=20form=20builder=20-=20twig=20trans=20filter=20alias=20-=20?= =?UTF-8?q?demonstrator=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/composer/autoload_classmap.php | 2 - lib/composer/autoload_static.php | 2 - lib/composer/installed.php | 4 +- .../TwigBase/Controller/Controller.php | 52 ++++++++++++++++--- .../Application/TwigBase/Twig/Extension.php | 5 ++ .../forms/itop_console_layout.twig | 2 +- 6 files changed, 54 insertions(+), 13 deletions(-) diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index fa3f3fbd94..2d2e759c11 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -144,8 +144,6 @@ 'Combodo\\iTop\\Application\\Search\\CriterionParser' => $baseDir . '/sources/Application/Search/criterionparser.class.inc.php', 'Combodo\\iTop\\Application\\Search\\SearchForm' => $baseDir . '/sources/Application/Search/searchform.class.inc.php', 'Combodo\\iTop\\Application\\Status\\Status' => $baseDir . '/sources/Application/Status/Status.php', - 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormController' => $baseDir . '/sources/Application/Symfony/Poc/BaseForm/BaseFormController.php', - 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormType' => $baseDir . '/sources/Application/Symfony/Poc/BaseForm/BaseFormType.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\Controller' => $baseDir . '/sources/Application/TwigBase/Controller/Controller.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\PageNotFoundException' => $baseDir . '/application/exceptions/PageNotFoundException.php', 'Combodo\\iTop\\Application\\TwigBase\\Twig\\Extension' => $baseDir . '/sources/Application/TwigBase/Twig/Extension.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 1b5fc85649..9f9b6fd5d5 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -525,8 +525,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Application\\Search\\CriterionParser' => __DIR__ . '/../..' . '/sources/Application/Search/criterionparser.class.inc.php', 'Combodo\\iTop\\Application\\Search\\SearchForm' => __DIR__ . '/../..' . '/sources/Application/Search/searchform.class.inc.php', 'Combodo\\iTop\\Application\\Status\\Status' => __DIR__ . '/../..' . '/sources/Application/Status/Status.php', - 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormController' => __DIR__ . '/../..' . '/sources/Application/Symfony/Poc/BaseForm/BaseFormController.php', - 'Combodo\\iTop\\Application\\Symfony\\Poc\\BaseForm\\BaseFormType' => __DIR__ . '/../..' . '/sources/Application/Symfony/Poc/BaseForm/BaseFormType.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\Controller' => __DIR__ . '/../..' . '/sources/Application/TwigBase/Controller/Controller.php', 'Combodo\\iTop\\Application\\TwigBase\\Controller\\PageNotFoundException' => __DIR__ . '/../..' . '/application/exceptions/PageNotFoundException.php', 'Combodo\\iTop\\Application\\TwigBase\\Twig\\Extension' => __DIR__ . '/../..' . '/sources/Application/TwigBase/Twig/Extension.php', diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 5010615635..57c8fa5e83 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '8f038d2f9529e3d1ffbb08b0dc05931e958f28fc', + 'reference' => '58b47824cad73d157fde4bbb319711551e0e0869', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '8f038d2f9529e3d1ffbb08b0dc05931e958f28fc', + 'reference' => '58b47824cad73d157fde4bbb319711551e0e0869', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index 7bc23b44e2..21a1423735 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -26,7 +26,6 @@ use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\Controller\AbstractController; -use Combodo\iTop\Forms\Forms; use Dict; use Exception; use ExecutionKPI; @@ -39,9 +38,12 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\Forms; use Symfony\Component\HttpFoundation\Request; use Twig\Error\Error; use Twig\Error\SyntaxError; @@ -49,8 +51,6 @@ use utils; use ZipArchive; -//use Combodo\iTop\Forms\Forms; - abstract class Controller extends AbstractController { const ENUM_PAGE_TYPE_HTML = 'html'; @@ -91,9 +91,13 @@ abstract class Controller extends AbstractController private $m_bIsBreadCrumbEnabled = true; /** @var array contains same parameters as {@see iTopWebPage::SetBreadCrumbEntry()} */ private $m_aBreadCrumbEntry = []; - /** @var \Symfony\Component\HttpFoundation\Request */ + + /** @var Request Request (from Symfony http_foundation component @link https://symfony.com/doc/current/components/http_foundation.html) */ private Request $oRequest; + /** @var FormFactoryBuilderInterface Factory form builder (from Symfony form component @link https://symfony.com/doc/current/components/form.html) */ + private FormFactoryBuilderInterface $oFormFactoryBuilder; + /** * Controller constructor. * @@ -102,7 +106,6 @@ abstract class Controller extends AbstractController */ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = []) { - $this->oRequest = Request::createFromGlobals(); $this->m_aLinkedScripts = []; $this->m_aLinkedStylesheets = []; $this->m_aSaas = []; @@ -123,6 +126,23 @@ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditional } } } + + // Initialize Symfony components + $this->InitSymfonyComponents();; + } + + /** + * Init controllers vars related to Symfony components. + * + * @return void + */ + private function InitSymfonyComponents(): void + { + // a request object representation from PHP request globals + $this->oRequest = Request::createFromGlobals(); + + // initialize the form factory builder to handle Request objects + $this->oFormFactoryBuilder = Forms::createFormFactoryBuilder()->addExtension(new HttpFoundationExtension()); } /** @@ -688,11 +708,31 @@ public function GetRequest(): Request return $this->oRequest; } + /** + * Get a form builder. + * This form builder can be used to create a form or to add fields to an existing form. + * + * @param string $type + * @param mixed|null $data + * @param array $options + * + * @return FormBuilderInterface + */ public function GetFormBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface { - return Forms::createFormFactory()->createBuilder($type, $data,$options); + return $this->oFormFactoryBuilder->getFormFactory()->createBuilder($type, $data,$options); } + /** + * Get a form. + * This form can be directly used in a twig template. + * + * @param string $type + * @param mixed|null $data + * @param array $options + * + * @return FormInterface + */ public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface { if (is_null($data)) { diff --git a/sources/Application/TwigBase/Twig/Extension.php b/sources/Application/TwigBase/Twig/Extension.php index 09ddb718bf..b1dfc68601 100644 --- a/sources/Application/TwigBase/Twig/Extension.php +++ b/sources/Application/TwigBase/Twig/Extension.php @@ -59,6 +59,11 @@ public static function GetFilters() return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); }); + // Alias of dict_s, to be compatible with Symfony/Twig standard + $aFilters[] = new TwigFilter('trans', function ($sStringCode, $aData = null, $sTransDomain = false) { + return Dict::S($sStringCode); + }); + // Filter to format a string via the Dict::Format function // Usage in twig: {{ 'String:ToTranslate'|dict_format() }} $aFilters[] = new TwigFilter('dict_format', function ($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null) { diff --git a/templates/application/forms/itop_console_layout.twig b/templates/application/forms/itop_console_layout.twig index 610fa26e3f..a89c1d04c2 100644 --- a/templates/application/forms/itop_console_layout.twig +++ b/templates/application/forms/itop_console_layout.twig @@ -1,4 +1,4 @@ -{% use "application/form/itop_base_layout.html.twig" %} +{% use "application/forms/itop_base_layout.html.twig" %} {# Widgets #} From dbaa807057792a1ead0ce8a17bf4f6143f8d2b97 Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Thu, 9 Oct 2025 07:55:37 +0200 Subject: [PATCH 05/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20remove=20itop=20forms=20enca?= =?UTF-8?q?psulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/composer/autoload_classmap.php | 1 - lib/composer/autoload_static.php | 1 - lib/composer/installed.php | 4 +-- sources/Forms/Forms.php | 42 ------------------------------ 4 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 sources/Forms/Forms.php diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 2d2e759c11..d4267c33c8 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -471,7 +471,6 @@ 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => $baseDir . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => $baseDir . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => $baseDir . '/sources/Form/Validator/SelectObjectValidator.php', - 'Combodo\\iTop\\Forms\\Forms' => $baseDir . '/sources/Forms/Forms.php', 'Combodo\\iTop\\Kernel' => $baseDir . '/sources/Kernel.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 9f9b6fd5d5..30adedc4c7 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -852,7 +852,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/SelectObjectValidator.php', - 'Combodo\\iTop\\Forms\\Forms' => __DIR__ . '/../..' . '/sources/Forms/Forms.php', 'Combodo\\iTop\\Kernel' => __DIR__ . '/../..' . '/sources/Kernel.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php', diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 57c8fa5e83..eaaf931c8e 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '58b47824cad73d157fde4bbb319711551e0e0869', + 'reference' => 'c07c3cdca1e8472cb54809021b0856d8682455c6', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '58b47824cad73d157fde4bbb319711551e0e0869', + 'reference' => 'c07c3cdca1e8472cb54809021b0856d8682455c6', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/sources/Forms/Forms.php b/sources/Forms/Forms.php deleted file mode 100644 index 8dab5bc770..0000000000 --- a/sources/Forms/Forms.php +++ /dev/null @@ -1,42 +0,0 @@ -getFormFactory(); - } - - /** - * Creates a form factory builder with the iTop configuration. - */ - public static function createFormFactoryBuilder(): FormFactoryBuilderInterface - { - return (new FormFactoryBuilder()) - ->addExtension(new HttpFoundationExtension()); - } - - /** - * This class cannot be instantiated. - */ - private function __construct() - { - } -} \ No newline at end of file From 246989598e0e417cb000753bb3da63fe259cf63a Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Thu, 9 Oct 2025 13:58:07 +0200 Subject: [PATCH 06/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20update=20lib=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/symfony/form/AbstractExtension.php | 179 +++ lib/symfony/form/AbstractRendererEngine.php | 197 +++ lib/symfony/form/AbstractType.php | 66 + lib/symfony/form/AbstractTypeExtension.php | 48 + lib/symfony/form/Button.php | 373 +++++ lib/symfony/form/ButtonBuilder.php | 738 +++++++++ lib/symfony/form/ButtonTypeInterface.php | 21 + lib/symfony/form/CHANGELOG.md | 616 ++++++++ lib/symfony/form/CallbackTransformer.php | 34 + .../form/ChoiceList/ArrayChoiceList.php | 219 +++ lib/symfony/form/ChoiceList/ChoiceList.php | 149 ++ .../form/ChoiceList/ChoiceListInterface.php | 138 ++ .../Factory/Cache/AbstractStaticOption.php | 55 + .../ChoiceList/Factory/Cache/ChoiceAttr.php | 27 + .../Factory/Cache/ChoiceFieldName.php | 27 + .../ChoiceList/Factory/Cache/ChoiceFilter.php | 27 + .../ChoiceList/Factory/Cache/ChoiceLabel.php | 27 + .../ChoiceList/Factory/Cache/ChoiceLoader.php | 43 + .../Cache/ChoiceTranslationParameters.php | 27 + .../ChoiceList/Factory/Cache/ChoiceValue.php | 27 + .../form/ChoiceList/Factory/Cache/GroupBy.php | 27 + .../Factory/Cache/PreferredChoice.php | 27 + .../Factory/CachingFactoryDecorator.php | 232 +++ .../Factory/ChoiceListFactoryInterface.php | 85 ++ .../Factory/DefaultChoiceListFactory.php | 307 ++++ .../Factory/PropertyAccessDecorator.php | 193 +++ .../form/ChoiceList/LazyChoiceList.php | 83 + .../Loader/AbstractChoiceLoader.php | 66 + .../Loader/CallbackChoiceLoader.php | 35 + .../Loader/ChoiceLoaderInterface.php | 72 + .../Loader/FilterChoiceLoaderDecorator.php | 64 + .../Loader/IntlCallbackChoiceLoader.php | 38 + .../form/ChoiceList/View/ChoiceGroupView.php | 44 + .../form/ChoiceList/View/ChoiceListView.php | 57 + .../form/ChoiceList/View/ChoiceView.php | 54 + lib/symfony/form/ClearableErrorsInterface.php | 29 + lib/symfony/form/ClickableInterface.php | 25 + lib/symfony/form/Command/DebugCommand.php | 291 ++++ .../form/Console/Descriptor/Descriptor.php | 195 +++ .../Console/Descriptor/JsonDescriptor.php | 118 ++ .../Console/Descriptor/TextDescriptor.php | 219 +++ .../form/Console/Helper/DescriptorHelper.php | 34 + lib/symfony/form/DataAccessorInterface.php | 50 + lib/symfony/form/DataMapperInterface.php | 66 + lib/symfony/form/DataTransformerInterface.php | 99 ++ .../form/DependencyInjection/FormPass.php | 130 ++ lib/symfony/form/Event/PostSetDataEvent.php | 33 + lib/symfony/form/Event/PostSubmitEvent.php | 33 + lib/symfony/form/Event/PreSetDataEvent.php | 23 + lib/symfony/form/Event/PreSubmitEvent.php | 25 + lib/symfony/form/Event/SubmitEvent.php | 24 + .../form/Exception/AccessException.php | 16 + .../Exception/AlreadySubmittedException.php | 22 + .../form/Exception/BadMethodCallException.php | 21 + .../form/Exception/ErrorMappingException.php | 16 + .../form/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../InvalidConfigurationException.php | 16 + lib/symfony/form/Exception/LogicException.php | 21 + .../form/Exception/OutOfBoundsException.php | 21 + .../form/Exception/RuntimeException.php | 21 + .../form/Exception/StringCastException.php | 16 + .../TransformationFailedException.php | 55 + .../Exception/UnexpectedTypeException.php | 20 + .../form/Extension/Core/CoreExtension.php | 89 ++ .../Core/DataAccessor/CallbackAccessor.php | 52 + .../Core/DataAccessor/ChainAccessor.php | 78 + .../DataAccessor/PropertyPathAccessor.php | 117 ++ .../Core/DataMapper/CheckboxListMapper.php | 63 + .../Extension/Core/DataMapper/DataMapper.php | 85 ++ .../Core/DataMapper/RadioListMapper.php | 66 + .../ArrayToPartsTransformer.php | 82 + .../BaseDateTimeTransformer.php | 60 + .../BooleanToStringTransformer.php | 83 + .../ChoiceToValueTransformer.php | 55 + .../ChoicesToValuesTransformer.php | 71 + .../DataTransformer/DataTransformerChain.php | 86 ++ .../DateIntervalToArrayTransformer.php | 169 +++ .../DateIntervalToStringTransformer.php | 99 ++ ...DateTimeImmutableToDateTimeTransformer.php | 65 + .../DateTimeToArrayTransformer.php | 179 +++ ...ateTimeToHtml5LocalDateTimeTransformer.php | 107 ++ .../DateTimeToLocalizedStringTransformer.php | 197 +++ .../DateTimeToRfc3339Transformer.php | 86 ++ .../DateTimeToStringTransformer.php | 135 ++ .../DateTimeToTimestampTransformer.php | 78 + .../DateTimeZoneToStringTransformer.php | 78 + .../IntegerToLocalizedStringTransformer.php | 56 + .../IntlTimeZoneToStringTransformer.php | 80 + .../MoneyToLocalizedStringTransformer.php | 70 + .../NumberToLocalizedStringTransformer.php | 231 +++ .../PercentToLocalizedStringTransformer.php | 239 +++ .../StringToFloatTransformer.php | 58 + .../UlidToStringTransformer.php | 73 + .../UuidToStringTransformer.php | 75 + .../ValueToDuplicatesTransformer.php | 81 + .../WeekToArrayTransformer.php | 107 ++ .../EventListener/FixUrlProtocolListener.php | 51 + .../EventListener/MergeCollectionListener.php | 112 ++ .../Core/EventListener/ResizeFormListener.php | 167 +++ .../TransformationFailureListener.php | 68 + .../Core/EventListener/TrimListener.php | 44 + .../form/Extension/Core/Type/BaseType.php | 159 ++ .../form/Extension/Core/Type/BirthdayType.php | 41 + .../form/Extension/Core/Type/ButtonType.php | 43 + .../form/Extension/Core/Type/CheckboxType.php | 72 + .../form/Extension/Core/Type/ChoiceType.php | 478 ++++++ .../Extension/Core/Type/CollectionType.php | 141 ++ .../form/Extension/Core/Type/ColorType.php | 88 ++ .../form/Extension/Core/Type/CountryType.php | 60 + .../form/Extension/Core/Type/CurrencyType.php | 57 + .../Extension/Core/Type/DateIntervalType.php | 274 ++++ .../form/Extension/Core/Type/DateTimeType.php | 357 +++++ .../form/Extension/Core/Type/DateType.php | 409 +++++ .../form/Extension/Core/Type/EmailType.php | 38 + .../form/Extension/Core/Type/EnumType.php | 54 + .../form/Extension/Core/Type/FileType.php | 243 +++ .../form/Extension/Core/Type/FormType.php | 218 +++ .../form/Extension/Core/Type/HiddenType.php | 38 + .../form/Extension/Core/Type/IntegerType.php | 69 + .../form/Extension/Core/Type/LanguageType.php | 86 ++ .../form/Extension/Core/Type/LocaleType.php | 57 + .../form/Extension/Core/Type/MoneyType.php | 144 ++ .../form/Extension/Core/Type/NumberType.php | 101 ++ .../form/Extension/Core/Type/PasswordType.php | 52 + .../form/Extension/Core/Type/PercentType.php | 85 ++ .../form/Extension/Core/Type/RadioType.php | 38 + .../form/Extension/Core/Type/RangeType.php | 38 + .../form/Extension/Core/Type/RepeatedType.php | 72 + .../form/Extension/Core/Type/ResetType.php | 33 + .../form/Extension/Core/Type/SearchType.php | 38 + .../form/Extension/Core/Type/SubmitType.php | 57 + .../form/Extension/Core/Type/TelType.php | 38 + .../form/Extension/Core/Type/TextType.php | 61 + .../form/Extension/Core/Type/TextareaType.php | 38 + .../form/Extension/Core/Type/TimeType.php | 398 +++++ .../form/Extension/Core/Type/TimezoneType.php | 129 ++ .../Type/TransformationFailureExtension.php | 45 + .../form/Extension/Core/Type/UlidType.php | 44 + .../form/Extension/Core/Type/UrlType.php | 66 + .../form/Extension/Core/Type/UuidType.php | 44 + .../form/Extension/Core/Type/WeekType.php | 181 +++ .../form/Extension/Csrf/CsrfExtension.php | 42 + .../EventListener/CsrfValidationListener.php | 84 ++ .../Csrf/Type/FormTypeCsrfExtension.php | 111 ++ .../DataCollector/DataCollectorExtension.php | 37 + .../EventListener/DataCollectorListener.php | 76 + .../DataCollector/FormDataCollector.php | 297 ++++ .../FormDataCollectorInterface.php | 97 ++ .../DataCollector/FormDataExtractor.php | 156 ++ .../FormDataExtractorInterface.php | 43 + .../Proxy/ResolvedTypeDataCollectorProxy.php | 121 ++ .../ResolvedTypeFactoryDataCollectorProxy.php | 43 + .../Type/DataCollectorTypeExtension.php | 47 + .../DependencyInjectionExtension.php | 99 ++ .../HtmlSanitizer/HtmlSanitizerExtension.php | 36 + .../Type/TextTypeHtmlSanitizerExtension.php | 72 + .../HttpFoundationExtension.php | 29 + .../HttpFoundationRequestHandler.php | 126 ++ .../Type/FormTypeHttpFoundationExtension.php | 44 + .../EventListener/PasswordHasherListener.php | 116 ++ .../PasswordHasherExtension.php | 36 + .../Type/FormTypePasswordHasherExtension.php | 42 + .../PasswordTypePasswordHasherExtension.php | 60 + .../Extension/Validator/Constraints/Form.php | 38 + .../Validator/Constraints/FormValidator.php | 279 ++++ .../EventListener/ValidationListener.php | 58 + .../Validator/Type/BaseValidatorExtension.php | 59 + .../Type/FormTypeValidatorExtension.php | 74 + .../Type/RepeatedTypeValidatorExtension.php | 41 + .../Type/SubmitTypeValidatorExtension.php | 25 + .../Type/UploadValidatorExtension.php | 49 + .../Validator/ValidatorExtension.php | 68 + .../Validator/ValidatorTypeGuesser.php | 293 ++++ .../Validator/ViolationMapper/MappingRule.php | 78 + .../ViolationMapper/RelativePath.php | 35 + .../ViolationMapper/ViolationMapper.php | 343 +++++ .../ViolationMapperInterface.php | 31 + .../ViolationMapper/ViolationPath.php | 217 +++ .../ViolationMapper/ViolationPathIterator.php | 33 + lib/symfony/form/FileUploadError.php | 19 + lib/symfony/form/Form.php | 1022 +++++++++++++ lib/symfony/form/FormBuilder.php | 217 +++ lib/symfony/form/FormBuilderInterface.php | 69 + lib/symfony/form/FormConfigBuilder.php | 657 ++++++++ .../form/FormConfigBuilderInterface.php | 260 ++++ lib/symfony/form/FormConfigInterface.php | 196 +++ lib/symfony/form/FormError.php | 122 ++ lib/symfony/form/FormErrorIterator.php | 275 ++++ lib/symfony/form/FormEvent.php | 55 + lib/symfony/form/FormEvents.php | 113 ++ lib/symfony/form/FormExtensionInterface.php | 55 + lib/symfony/form/FormFactory.php | 104 ++ lib/symfony/form/FormFactoryBuilder.php | 154 ++ .../form/FormFactoryBuilderInterface.php | 96 ++ lib/symfony/form/FormFactoryInterface.php | 90 ++ lib/symfony/form/FormInterface.php | 289 ++++ lib/symfony/form/FormRegistry.php | 156 ++ lib/symfony/form/FormRegistryInterface.php | 46 + lib/symfony/form/FormRenderer.php | 288 ++++ .../form/FormRendererEngineInterface.php | 136 ++ lib/symfony/form/FormRendererInterface.php | 87 ++ .../form/FormTypeExtensionInterface.php | 74 + lib/symfony/form/FormTypeGuesserChain.php | 85 ++ lib/symfony/form/FormTypeGuesserInterface.php | 46 + lib/symfony/form/FormTypeInterface.php | 98 ++ lib/symfony/form/FormView.php | 157 ++ lib/symfony/form/Forms.php | 87 ++ lib/symfony/form/Guess/Guess.php | 101 ++ lib/symfony/form/Guess/TypeGuess.php | 55 + lib/symfony/form/Guess/ValueGuess.php | 40 + lib/symfony/form/LICENSE | 19 + lib/symfony/form/NativeRequestHandler.php | 241 +++ lib/symfony/form/PreloadedExtension.php | 72 + lib/symfony/form/README.md | 13 + lib/symfony/form/RequestHandlerInterface.php | 32 + lib/symfony/form/ResolvedFormType.php | 184 +++ lib/symfony/form/ResolvedFormTypeFactory.php | 23 + .../form/ResolvedFormTypeFactoryInterface.php | 34 + .../form/ResolvedFormTypeInterface.php | 86 ++ .../form/Resources/config/validation.xml | 13 + .../Resources/translations/validators.af.xlf | 139 ++ .../Resources/translations/validators.ar.xlf | 139 ++ .../Resources/translations/validators.az.xlf | 139 ++ .../Resources/translations/validators.be.xlf | 139 ++ .../Resources/translations/validators.bg.xlf | 139 ++ .../Resources/translations/validators.bs.xlf | 139 ++ .../Resources/translations/validators.ca.xlf | 139 ++ .../Resources/translations/validators.cs.xlf | 139 ++ .../Resources/translations/validators.cy.xlf | 139 ++ .../Resources/translations/validators.da.xlf | 139 ++ .../Resources/translations/validators.de.xlf | 139 ++ .../Resources/translations/validators.el.xlf | 139 ++ .../Resources/translations/validators.en.xlf | 139 ++ .../Resources/translations/validators.es.xlf | 139 ++ .../Resources/translations/validators.et.xlf | 139 ++ .../Resources/translations/validators.eu.xlf | 139 ++ .../Resources/translations/validators.fa.xlf | 139 ++ .../Resources/translations/validators.fi.xlf | 139 ++ .../Resources/translations/validators.fr.xlf | 139 ++ .../Resources/translations/validators.gl.xlf | 139 ++ .../Resources/translations/validators.he.xlf | 139 ++ .../Resources/translations/validators.hr.xlf | 139 ++ .../Resources/translations/validators.hu.xlf | 139 ++ .../Resources/translations/validators.hy.xlf | 139 ++ .../Resources/translations/validators.id.xlf | 139 ++ .../Resources/translations/validators.it.xlf | 139 ++ .../Resources/translations/validators.ja.xlf | 139 ++ .../Resources/translations/validators.lb.xlf | 139 ++ .../Resources/translations/validators.lt.xlf | 139 ++ .../Resources/translations/validators.lv.xlf | 139 ++ .../Resources/translations/validators.mk.xlf | 139 ++ .../Resources/translations/validators.mn.xlf | 139 ++ .../Resources/translations/validators.my.xlf | 139 ++ .../Resources/translations/validators.nb.xlf | 139 ++ .../Resources/translations/validators.nl.xlf | 139 ++ .../Resources/translations/validators.nn.xlf | 139 ++ .../Resources/translations/validators.no.xlf | 139 ++ .../Resources/translations/validators.pl.xlf | 139 ++ .../Resources/translations/validators.pt.xlf | 139 ++ .../translations/validators.pt_BR.xlf | 139 ++ .../Resources/translations/validators.ro.xlf | 139 ++ .../Resources/translations/validators.ru.xlf | 139 ++ .../Resources/translations/validators.sk.xlf | 139 ++ .../Resources/translations/validators.sl.xlf | 139 ++ .../Resources/translations/validators.sq.xlf | 148 ++ .../translations/validators.sr_Cyrl.xlf | 139 ++ .../translations/validators.sr_Latn.xlf | 139 ++ .../Resources/translations/validators.sv.xlf | 139 ++ .../Resources/translations/validators.th.xlf | 139 ++ .../Resources/translations/validators.tl.xlf | 139 ++ .../Resources/translations/validators.tr.xlf | 139 ++ .../Resources/translations/validators.uk.xlf | 139 ++ .../Resources/translations/validators.ur.xlf | 139 ++ .../Resources/translations/validators.uz.xlf | 139 ++ .../Resources/translations/validators.vi.xlf | 139 ++ .../translations/validators.zh_CN.xlf | 139 ++ .../translations/validators.zh_TW.xlf | 139 ++ lib/symfony/form/ReversedTransformer.php | 40 + lib/symfony/form/SubmitButton.php | 49 + lib/symfony/form/SubmitButtonBuilder.php | 28 + .../form/SubmitButtonTypeInterface.php | 21 + .../form/Test/FormBuilderInterface.php | 18 + .../form/Test/FormIntegrationTestCase.php | 54 + lib/symfony/form/Test/FormInterface.php | 18 + .../form/Test/FormPerformanceTestCase.php | 64 + lib/symfony/form/Test/Traits/RunTestTrait.php | 36 + .../Test/Traits/ValidatorExtensionTrait.php | 44 + lib/symfony/form/Test/TypeTestCase.php | 58 + lib/symfony/form/Util/FormUtil.php | 68 + .../form/Util/InheritDataAwareIterator.php | 37 + .../form/Util/OptionsResolverWrapper.php | 110 ++ lib/symfony/form/Util/OrderedHashMap.php | 162 ++ .../form/Util/OrderedHashMapIterator.php | 125 ++ lib/symfony/form/Util/ServerParams.php | 93 ++ lib/symfony/form/Util/StringUtil.php | 53 + lib/symfony/form/composer.json | 64 + lib/symfony/options-resolver/CHANGELOG.md | 96 ++ .../Debug/OptionsResolverIntrospector.php | 104 ++ .../Exception/AccessException.php | 22 + .../Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../Exception/InvalidOptionsException.php | 23 + .../Exception/MissingOptionsException.php | 23 + .../Exception/NoConfigurationException.php | 26 + .../Exception/NoSuchOptionException.php | 26 + .../Exception/OptionDefinitionException.php | 21 + .../Exception/UndefinedOptionsException.php | 24 + lib/symfony/options-resolver/LICENSE | 19 + .../options-resolver/OptionConfigurator.php | 149 ++ lib/symfony/options-resolver/Options.php | 22 + .../options-resolver/OptionsResolver.php | 1317 ++++++++++++++++ lib/symfony/options-resolver/README.md | 15 + lib/symfony/options-resolver/composer.json | 29 + lib/symfony/polyfill-intl-icu/Collator.php | 262 ++++ lib/symfony/polyfill-intl-icu/Currencies.php | 43 + .../DateFormat/AmPmTransformer.php | 39 + .../DateFormat/DayOfWeekTransformer.php | 56 + .../DateFormat/DayOfYearTransformer.php | 39 + .../DateFormat/DayTransformer.php | 39 + .../DateFormat/FullTransformer.php | 312 ++++ .../DateFormat/Hour1200Transformer.php | 52 + .../DateFormat/Hour1201Transformer.php | 52 + .../DateFormat/Hour2400Transformer.php | 51 + .../DateFormat/Hour2401Transformer.php | 54 + .../DateFormat/HourTransformer.php | 32 + .../DateFormat/MinuteTransformer.php | 41 + .../DateFormat/MonthTransformer.php | 127 ++ .../DateFormat/QuarterTransformer.php | 65 + .../DateFormat/SecondTransformer.php | 41 + .../DateFormat/TimezoneTransformer.php | 108 ++ .../DateFormat/Transformer.php | 65 + .../DateFormat/YearTransformer.php | 43 + .../Exception/ExceptionInterface.php | 21 + .../MethodArgumentNotImplementedException.php | 28 + ...odArgumentValueNotImplementedException.php | 37 + .../MethodNotImplementedException.php | 26 + .../Exception/NotImplementedException.php | 30 + .../Exception/RuntimeException.php | 21 + lib/symfony/polyfill-intl-icu/Icu.php | 117 ++ .../polyfill-intl-icu/IntlDateFormatter.php | 645 ++++++++ lib/symfony/polyfill-intl-icu/LICENSE | 19 + lib/symfony/polyfill-intl-icu/Locale.php | 310 ++++ .../polyfill-intl-icu/NumberFormatter.php | 835 +++++++++++ lib/symfony/polyfill-intl-icu/README.md | 23 + .../Resources/currencies.php | 1329 +++++++++++++++++ .../Resources/stubs/Collator.php | 21 + .../Resources/stubs/IntlDateFormatter.php | 21 + .../Resources/stubs/Locale.php | 21 + .../Resources/stubs/NumberFormatter.php | 23 + lib/symfony/polyfill-intl-icu/bootstrap.php | 33 + lib/symfony/polyfill-intl-icu/bootstrap80.php | 25 + lib/symfony/polyfill-intl-icu/composer.json | 39 + lib/symfony/property-access/CHANGELOG.md | 88 ++ .../Exception/AccessException.php | 21 + .../Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../InvalidPropertyPathException.php | 21 + .../Exception/NoSuchIndexException.php | 21 + .../Exception/NoSuchPropertyException.php | 21 + .../Exception/OutOfBoundsException.php | 21 + .../Exception/RuntimeException.php | 21 + .../Exception/UnexpectedTypeException.php | 39 + .../UninitializedPropertyException.php | 21 + lib/symfony/property-access/LICENSE | 19 + .../property-access/PropertyAccess.php | 40 + .../property-access/PropertyAccessor.php | 715 +++++++++ .../PropertyAccessorBuilder.php | 294 ++++ .../PropertyAccessorInterface.php | 97 ++ lib/symfony/property-access/PropertyPath.php | 205 +++ .../property-access/PropertyPathBuilder.php | 275 ++++ .../property-access/PropertyPathInterface.php | 88 ++ .../property-access/PropertyPathIterator.php | 42 + .../PropertyPathIteratorInterface.php | 32 + lib/symfony/property-access/README.md | 14 + lib/symfony/property-access/composer.json | 33 + lib/symfony/property-info/CHANGELOG.md | 57 + .../PropertyInfoConstructorPass.php | 38 + .../DependencyInjection/PropertyInfoPass.php | 54 + ...structorArgumentTypeExtractorInterface.php | 33 + .../Extractor/ConstructorExtractor.php | 42 + .../Extractor/PhpDocExtractor.php | 348 +++++ .../Extractor/PhpStanExtractor.php | 323 ++++ .../Extractor/ReflectionExtractor.php | 872 +++++++++++ .../Extractor/SerializerExtractor.php | 52 + lib/symfony/property-info/LICENSE | 19 + .../property-info/PhpStan/NameScope.php | 65 + .../PhpStan/NameScopeFactory.php | 70 + .../PropertyAccessExtractorInterface.php | 34 + .../PropertyDescriptionExtractorInterface.php | 30 + .../PropertyInfoCacheExtractor.php | 99 ++ .../property-info/PropertyInfoExtractor.php | 90 ++ .../PropertyInfoExtractorInterface.php | 23 + ...ropertyInitializableExtractorInterface.php | 25 + .../PropertyListExtractorInterface.php | 27 + .../property-info/PropertyReadInfo.php | 70 + .../PropertyReadInfoExtractorInterface.php | 25 + .../PropertyTypeExtractorInterface.php | 27 + .../property-info/PropertyWriteInfo.php | 117 ++ .../PropertyWriteInfoExtractorInterface.php | 25 + lib/symfony/property-info/README.md | 14 + lib/symfony/property-info/Type.php | 165 ++ .../property-info/Util/PhpDocTypeHelper.php | 198 +++ .../property-info/Util/PhpStanTypeHelper.php | 211 +++ lib/symfony/property-info/composer.json | 52 + 405 files changed, 45532 insertions(+) create mode 100644 lib/symfony/form/AbstractExtension.php create mode 100644 lib/symfony/form/AbstractRendererEngine.php create mode 100644 lib/symfony/form/AbstractType.php create mode 100644 lib/symfony/form/AbstractTypeExtension.php create mode 100644 lib/symfony/form/Button.php create mode 100644 lib/symfony/form/ButtonBuilder.php create mode 100644 lib/symfony/form/ButtonTypeInterface.php create mode 100644 lib/symfony/form/CHANGELOG.md create mode 100644 lib/symfony/form/CallbackTransformer.php create mode 100644 lib/symfony/form/ChoiceList/ArrayChoiceList.php create mode 100644 lib/symfony/form/ChoiceList/ChoiceList.php create mode 100644 lib/symfony/form/ChoiceList/ChoiceListInterface.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/AbstractStaticOption.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceAttr.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFieldName.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFilter.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLabel.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLoader.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceTranslationParameters.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/ChoiceValue.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/GroupBy.php create mode 100644 lib/symfony/form/ChoiceList/Factory/Cache/PreferredChoice.php create mode 100644 lib/symfony/form/ChoiceList/Factory/CachingFactoryDecorator.php create mode 100644 lib/symfony/form/ChoiceList/Factory/ChoiceListFactoryInterface.php create mode 100644 lib/symfony/form/ChoiceList/Factory/DefaultChoiceListFactory.php create mode 100644 lib/symfony/form/ChoiceList/Factory/PropertyAccessDecorator.php create mode 100644 lib/symfony/form/ChoiceList/LazyChoiceList.php create mode 100644 lib/symfony/form/ChoiceList/Loader/AbstractChoiceLoader.php create mode 100644 lib/symfony/form/ChoiceList/Loader/CallbackChoiceLoader.php create mode 100644 lib/symfony/form/ChoiceList/Loader/ChoiceLoaderInterface.php create mode 100644 lib/symfony/form/ChoiceList/Loader/FilterChoiceLoaderDecorator.php create mode 100644 lib/symfony/form/ChoiceList/Loader/IntlCallbackChoiceLoader.php create mode 100644 lib/symfony/form/ChoiceList/View/ChoiceGroupView.php create mode 100644 lib/symfony/form/ChoiceList/View/ChoiceListView.php create mode 100644 lib/symfony/form/ChoiceList/View/ChoiceView.php create mode 100644 lib/symfony/form/ClearableErrorsInterface.php create mode 100644 lib/symfony/form/ClickableInterface.php create mode 100644 lib/symfony/form/Command/DebugCommand.php create mode 100644 lib/symfony/form/Console/Descriptor/Descriptor.php create mode 100644 lib/symfony/form/Console/Descriptor/JsonDescriptor.php create mode 100644 lib/symfony/form/Console/Descriptor/TextDescriptor.php create mode 100644 lib/symfony/form/Console/Helper/DescriptorHelper.php create mode 100644 lib/symfony/form/DataAccessorInterface.php create mode 100644 lib/symfony/form/DataMapperInterface.php create mode 100644 lib/symfony/form/DataTransformerInterface.php create mode 100644 lib/symfony/form/DependencyInjection/FormPass.php create mode 100644 lib/symfony/form/Event/PostSetDataEvent.php create mode 100644 lib/symfony/form/Event/PostSubmitEvent.php create mode 100644 lib/symfony/form/Event/PreSetDataEvent.php create mode 100644 lib/symfony/form/Event/PreSubmitEvent.php create mode 100644 lib/symfony/form/Event/SubmitEvent.php create mode 100644 lib/symfony/form/Exception/AccessException.php create mode 100644 lib/symfony/form/Exception/AlreadySubmittedException.php create mode 100644 lib/symfony/form/Exception/BadMethodCallException.php create mode 100644 lib/symfony/form/Exception/ErrorMappingException.php create mode 100644 lib/symfony/form/Exception/ExceptionInterface.php create mode 100644 lib/symfony/form/Exception/InvalidArgumentException.php create mode 100644 lib/symfony/form/Exception/InvalidConfigurationException.php create mode 100644 lib/symfony/form/Exception/LogicException.php create mode 100644 lib/symfony/form/Exception/OutOfBoundsException.php create mode 100644 lib/symfony/form/Exception/RuntimeException.php create mode 100644 lib/symfony/form/Exception/StringCastException.php create mode 100644 lib/symfony/form/Exception/TransformationFailedException.php create mode 100644 lib/symfony/form/Exception/UnexpectedTypeException.php create mode 100644 lib/symfony/form/Extension/Core/CoreExtension.php create mode 100644 lib/symfony/form/Extension/Core/DataAccessor/CallbackAccessor.php create mode 100644 lib/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php create mode 100644 lib/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php create mode 100644 lib/symfony/form/Extension/Core/DataMapper/CheckboxListMapper.php create mode 100644 lib/symfony/form/Extension/Core/DataMapper/DataMapper.php create mode 100644 lib/symfony/form/Extension/Core/DataMapper/RadioListMapper.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/ArrayToPartsTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/BaseDateTimeTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/BooleanToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DataTransformerChain.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/StringToFloatTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/UlidToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/UuidToStringTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php create mode 100644 lib/symfony/form/Extension/Core/DataTransformer/WeekToArrayTransformer.php create mode 100644 lib/symfony/form/Extension/Core/EventListener/FixUrlProtocolListener.php create mode 100644 lib/symfony/form/Extension/Core/EventListener/MergeCollectionListener.php create mode 100644 lib/symfony/form/Extension/Core/EventListener/ResizeFormListener.php create mode 100644 lib/symfony/form/Extension/Core/EventListener/TransformationFailureListener.php create mode 100644 lib/symfony/form/Extension/Core/EventListener/TrimListener.php create mode 100644 lib/symfony/form/Extension/Core/Type/BaseType.php create mode 100644 lib/symfony/form/Extension/Core/Type/BirthdayType.php create mode 100644 lib/symfony/form/Extension/Core/Type/ButtonType.php create mode 100644 lib/symfony/form/Extension/Core/Type/CheckboxType.php create mode 100644 lib/symfony/form/Extension/Core/Type/ChoiceType.php create mode 100644 lib/symfony/form/Extension/Core/Type/CollectionType.php create mode 100644 lib/symfony/form/Extension/Core/Type/ColorType.php create mode 100644 lib/symfony/form/Extension/Core/Type/CountryType.php create mode 100644 lib/symfony/form/Extension/Core/Type/CurrencyType.php create mode 100644 lib/symfony/form/Extension/Core/Type/DateIntervalType.php create mode 100644 lib/symfony/form/Extension/Core/Type/DateTimeType.php create mode 100644 lib/symfony/form/Extension/Core/Type/DateType.php create mode 100644 lib/symfony/form/Extension/Core/Type/EmailType.php create mode 100644 lib/symfony/form/Extension/Core/Type/EnumType.php create mode 100644 lib/symfony/form/Extension/Core/Type/FileType.php create mode 100644 lib/symfony/form/Extension/Core/Type/FormType.php create mode 100644 lib/symfony/form/Extension/Core/Type/HiddenType.php create mode 100644 lib/symfony/form/Extension/Core/Type/IntegerType.php create mode 100644 lib/symfony/form/Extension/Core/Type/LanguageType.php create mode 100644 lib/symfony/form/Extension/Core/Type/LocaleType.php create mode 100644 lib/symfony/form/Extension/Core/Type/MoneyType.php create mode 100644 lib/symfony/form/Extension/Core/Type/NumberType.php create mode 100644 lib/symfony/form/Extension/Core/Type/PasswordType.php create mode 100644 lib/symfony/form/Extension/Core/Type/PercentType.php create mode 100644 lib/symfony/form/Extension/Core/Type/RadioType.php create mode 100644 lib/symfony/form/Extension/Core/Type/RangeType.php create mode 100644 lib/symfony/form/Extension/Core/Type/RepeatedType.php create mode 100644 lib/symfony/form/Extension/Core/Type/ResetType.php create mode 100644 lib/symfony/form/Extension/Core/Type/SearchType.php create mode 100644 lib/symfony/form/Extension/Core/Type/SubmitType.php create mode 100644 lib/symfony/form/Extension/Core/Type/TelType.php create mode 100644 lib/symfony/form/Extension/Core/Type/TextType.php create mode 100644 lib/symfony/form/Extension/Core/Type/TextareaType.php create mode 100644 lib/symfony/form/Extension/Core/Type/TimeType.php create mode 100644 lib/symfony/form/Extension/Core/Type/TimezoneType.php create mode 100644 lib/symfony/form/Extension/Core/Type/TransformationFailureExtension.php create mode 100644 lib/symfony/form/Extension/Core/Type/UlidType.php create mode 100644 lib/symfony/form/Extension/Core/Type/UrlType.php create mode 100644 lib/symfony/form/Extension/Core/Type/UuidType.php create mode 100644 lib/symfony/form/Extension/Core/Type/WeekType.php create mode 100644 lib/symfony/form/Extension/Csrf/CsrfExtension.php create mode 100644 lib/symfony/form/Extension/Csrf/EventListener/CsrfValidationListener.php create mode 100644 lib/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php create mode 100644 lib/symfony/form/Extension/DataCollector/DataCollectorExtension.php create mode 100644 lib/symfony/form/Extension/DataCollector/EventListener/DataCollectorListener.php create mode 100644 lib/symfony/form/Extension/DataCollector/FormDataCollector.php create mode 100644 lib/symfony/form/Extension/DataCollector/FormDataCollectorInterface.php create mode 100644 lib/symfony/form/Extension/DataCollector/FormDataExtractor.php create mode 100644 lib/symfony/form/Extension/DataCollector/FormDataExtractorInterface.php create mode 100644 lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php create mode 100644 lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php create mode 100644 lib/symfony/form/Extension/DataCollector/Type/DataCollectorTypeExtension.php create mode 100644 lib/symfony/form/Extension/DependencyInjection/DependencyInjectionExtension.php create mode 100644 lib/symfony/form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php create mode 100644 lib/symfony/form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php create mode 100644 lib/symfony/form/Extension/HttpFoundation/HttpFoundationExtension.php create mode 100644 lib/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php create mode 100644 lib/symfony/form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php create mode 100644 lib/symfony/form/Extension/PasswordHasher/EventListener/PasswordHasherListener.php create mode 100644 lib/symfony/form/Extension/PasswordHasher/PasswordHasherExtension.php create mode 100644 lib/symfony/form/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php create mode 100644 lib/symfony/form/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php create mode 100644 lib/symfony/form/Extension/Validator/Constraints/Form.php create mode 100644 lib/symfony/form/Extension/Validator/Constraints/FormValidator.php create mode 100644 lib/symfony/form/Extension/Validator/EventListener/ValidationListener.php create mode 100644 lib/symfony/form/Extension/Validator/Type/BaseValidatorExtension.php create mode 100644 lib/symfony/form/Extension/Validator/Type/FormTypeValidatorExtension.php create mode 100644 lib/symfony/form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php create mode 100644 lib/symfony/form/Extension/Validator/Type/SubmitTypeValidatorExtension.php create mode 100644 lib/symfony/form/Extension/Validator/Type/UploadValidatorExtension.php create mode 100644 lib/symfony/form/Extension/Validator/ValidatorExtension.php create mode 100644 lib/symfony/form/Extension/Validator/ValidatorTypeGuesser.php create mode 100644 lib/symfony/form/Extension/Validator/ViolationMapper/MappingRule.php create mode 100644 lib/symfony/form/Extension/Validator/ViolationMapper/RelativePath.php create mode 100644 lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapper.php create mode 100644 lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php create mode 100644 lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPath.php create mode 100644 lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPathIterator.php create mode 100644 lib/symfony/form/FileUploadError.php create mode 100644 lib/symfony/form/Form.php create mode 100644 lib/symfony/form/FormBuilder.php create mode 100644 lib/symfony/form/FormBuilderInterface.php create mode 100644 lib/symfony/form/FormConfigBuilder.php create mode 100644 lib/symfony/form/FormConfigBuilderInterface.php create mode 100644 lib/symfony/form/FormConfigInterface.php create mode 100644 lib/symfony/form/FormError.php create mode 100644 lib/symfony/form/FormErrorIterator.php create mode 100644 lib/symfony/form/FormEvent.php create mode 100644 lib/symfony/form/FormEvents.php create mode 100644 lib/symfony/form/FormExtensionInterface.php create mode 100644 lib/symfony/form/FormFactory.php create mode 100644 lib/symfony/form/FormFactoryBuilder.php create mode 100644 lib/symfony/form/FormFactoryBuilderInterface.php create mode 100644 lib/symfony/form/FormFactoryInterface.php create mode 100644 lib/symfony/form/FormInterface.php create mode 100644 lib/symfony/form/FormRegistry.php create mode 100644 lib/symfony/form/FormRegistryInterface.php create mode 100644 lib/symfony/form/FormRenderer.php create mode 100644 lib/symfony/form/FormRendererEngineInterface.php create mode 100644 lib/symfony/form/FormRendererInterface.php create mode 100644 lib/symfony/form/FormTypeExtensionInterface.php create mode 100644 lib/symfony/form/FormTypeGuesserChain.php create mode 100644 lib/symfony/form/FormTypeGuesserInterface.php create mode 100644 lib/symfony/form/FormTypeInterface.php create mode 100644 lib/symfony/form/FormView.php create mode 100644 lib/symfony/form/Forms.php create mode 100644 lib/symfony/form/Guess/Guess.php create mode 100644 lib/symfony/form/Guess/TypeGuess.php create mode 100644 lib/symfony/form/Guess/ValueGuess.php create mode 100644 lib/symfony/form/LICENSE create mode 100644 lib/symfony/form/NativeRequestHandler.php create mode 100644 lib/symfony/form/PreloadedExtension.php create mode 100644 lib/symfony/form/README.md create mode 100644 lib/symfony/form/RequestHandlerInterface.php create mode 100644 lib/symfony/form/ResolvedFormType.php create mode 100644 lib/symfony/form/ResolvedFormTypeFactory.php create mode 100644 lib/symfony/form/ResolvedFormTypeFactoryInterface.php create mode 100644 lib/symfony/form/ResolvedFormTypeInterface.php create mode 100644 lib/symfony/form/Resources/config/validation.xml create mode 100644 lib/symfony/form/Resources/translations/validators.af.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.ar.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.az.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.be.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.bg.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.bs.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.ca.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.cs.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.cy.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.da.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.de.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.el.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.en.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.es.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.et.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.eu.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.fa.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.fi.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.fr.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.gl.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.he.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.hr.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.hu.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.hy.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.id.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.it.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.ja.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.lb.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.lt.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.lv.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.mk.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.mn.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.my.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.nb.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.nl.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.nn.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.no.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.pl.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.pt.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.pt_BR.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.ro.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.ru.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.sk.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.sl.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.sq.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.sr_Cyrl.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.sr_Latn.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.sv.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.th.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.tl.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.tr.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.uk.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.ur.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.uz.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.vi.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.zh_CN.xlf create mode 100644 lib/symfony/form/Resources/translations/validators.zh_TW.xlf create mode 100644 lib/symfony/form/ReversedTransformer.php create mode 100644 lib/symfony/form/SubmitButton.php create mode 100644 lib/symfony/form/SubmitButtonBuilder.php create mode 100644 lib/symfony/form/SubmitButtonTypeInterface.php create mode 100644 lib/symfony/form/Test/FormBuilderInterface.php create mode 100644 lib/symfony/form/Test/FormIntegrationTestCase.php create mode 100644 lib/symfony/form/Test/FormInterface.php create mode 100644 lib/symfony/form/Test/FormPerformanceTestCase.php create mode 100644 lib/symfony/form/Test/Traits/RunTestTrait.php create mode 100644 lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php create mode 100644 lib/symfony/form/Test/TypeTestCase.php create mode 100644 lib/symfony/form/Util/FormUtil.php create mode 100644 lib/symfony/form/Util/InheritDataAwareIterator.php create mode 100644 lib/symfony/form/Util/OptionsResolverWrapper.php create mode 100644 lib/symfony/form/Util/OrderedHashMap.php create mode 100644 lib/symfony/form/Util/OrderedHashMapIterator.php create mode 100644 lib/symfony/form/Util/ServerParams.php create mode 100644 lib/symfony/form/Util/StringUtil.php create mode 100644 lib/symfony/form/composer.json create mode 100644 lib/symfony/options-resolver/CHANGELOG.md create mode 100644 lib/symfony/options-resolver/Debug/OptionsResolverIntrospector.php create mode 100644 lib/symfony/options-resolver/Exception/AccessException.php create mode 100644 lib/symfony/options-resolver/Exception/ExceptionInterface.php create mode 100644 lib/symfony/options-resolver/Exception/InvalidArgumentException.php create mode 100644 lib/symfony/options-resolver/Exception/InvalidOptionsException.php create mode 100644 lib/symfony/options-resolver/Exception/MissingOptionsException.php create mode 100644 lib/symfony/options-resolver/Exception/NoConfigurationException.php create mode 100644 lib/symfony/options-resolver/Exception/NoSuchOptionException.php create mode 100644 lib/symfony/options-resolver/Exception/OptionDefinitionException.php create mode 100644 lib/symfony/options-resolver/Exception/UndefinedOptionsException.php create mode 100644 lib/symfony/options-resolver/LICENSE create mode 100644 lib/symfony/options-resolver/OptionConfigurator.php create mode 100644 lib/symfony/options-resolver/Options.php create mode 100644 lib/symfony/options-resolver/OptionsResolver.php create mode 100644 lib/symfony/options-resolver/README.md create mode 100644 lib/symfony/options-resolver/composer.json create mode 100644 lib/symfony/polyfill-intl-icu/Collator.php create mode 100644 lib/symfony/polyfill-intl-icu/Currencies.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/AmPmTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/DayOfWeekTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/DayOfYearTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/DayTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/Hour1200Transformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/Hour1201Transformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/Hour2400Transformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/Hour2401Transformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/HourTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/MinuteTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/MonthTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/QuarterTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/SecondTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/TimezoneTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/Transformer.php create mode 100644 lib/symfony/polyfill-intl-icu/DateFormat/YearTransformer.php create mode 100644 lib/symfony/polyfill-intl-icu/Exception/ExceptionInterface.php create mode 100644 lib/symfony/polyfill-intl-icu/Exception/MethodArgumentNotImplementedException.php create mode 100644 lib/symfony/polyfill-intl-icu/Exception/MethodArgumentValueNotImplementedException.php create mode 100644 lib/symfony/polyfill-intl-icu/Exception/MethodNotImplementedException.php create mode 100644 lib/symfony/polyfill-intl-icu/Exception/NotImplementedException.php create mode 100644 lib/symfony/polyfill-intl-icu/Exception/RuntimeException.php create mode 100644 lib/symfony/polyfill-intl-icu/Icu.php create mode 100644 lib/symfony/polyfill-intl-icu/IntlDateFormatter.php create mode 100644 lib/symfony/polyfill-intl-icu/LICENSE create mode 100644 lib/symfony/polyfill-intl-icu/Locale.php create mode 100644 lib/symfony/polyfill-intl-icu/NumberFormatter.php create mode 100644 lib/symfony/polyfill-intl-icu/README.md create mode 100644 lib/symfony/polyfill-intl-icu/Resources/currencies.php create mode 100644 lib/symfony/polyfill-intl-icu/Resources/stubs/Collator.php create mode 100644 lib/symfony/polyfill-intl-icu/Resources/stubs/IntlDateFormatter.php create mode 100644 lib/symfony/polyfill-intl-icu/Resources/stubs/Locale.php create mode 100644 lib/symfony/polyfill-intl-icu/Resources/stubs/NumberFormatter.php create mode 100644 lib/symfony/polyfill-intl-icu/bootstrap.php create mode 100644 lib/symfony/polyfill-intl-icu/bootstrap80.php create mode 100644 lib/symfony/polyfill-intl-icu/composer.json create mode 100644 lib/symfony/property-access/CHANGELOG.md create mode 100644 lib/symfony/property-access/Exception/AccessException.php create mode 100644 lib/symfony/property-access/Exception/ExceptionInterface.php create mode 100644 lib/symfony/property-access/Exception/InvalidArgumentException.php create mode 100644 lib/symfony/property-access/Exception/InvalidPropertyPathException.php create mode 100644 lib/symfony/property-access/Exception/NoSuchIndexException.php create mode 100644 lib/symfony/property-access/Exception/NoSuchPropertyException.php create mode 100644 lib/symfony/property-access/Exception/OutOfBoundsException.php create mode 100644 lib/symfony/property-access/Exception/RuntimeException.php create mode 100644 lib/symfony/property-access/Exception/UnexpectedTypeException.php create mode 100644 lib/symfony/property-access/Exception/UninitializedPropertyException.php create mode 100644 lib/symfony/property-access/LICENSE create mode 100644 lib/symfony/property-access/PropertyAccess.php create mode 100644 lib/symfony/property-access/PropertyAccessor.php create mode 100644 lib/symfony/property-access/PropertyAccessorBuilder.php create mode 100644 lib/symfony/property-access/PropertyAccessorInterface.php create mode 100644 lib/symfony/property-access/PropertyPath.php create mode 100644 lib/symfony/property-access/PropertyPathBuilder.php create mode 100644 lib/symfony/property-access/PropertyPathInterface.php create mode 100644 lib/symfony/property-access/PropertyPathIterator.php create mode 100644 lib/symfony/property-access/PropertyPathIteratorInterface.php create mode 100644 lib/symfony/property-access/README.md create mode 100644 lib/symfony/property-access/composer.json create mode 100644 lib/symfony/property-info/CHANGELOG.md create mode 100644 lib/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php create mode 100644 lib/symfony/property-info/DependencyInjection/PropertyInfoPass.php create mode 100644 lib/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php create mode 100644 lib/symfony/property-info/Extractor/ConstructorExtractor.php create mode 100644 lib/symfony/property-info/Extractor/PhpDocExtractor.php create mode 100644 lib/symfony/property-info/Extractor/PhpStanExtractor.php create mode 100644 lib/symfony/property-info/Extractor/ReflectionExtractor.php create mode 100644 lib/symfony/property-info/Extractor/SerializerExtractor.php create mode 100644 lib/symfony/property-info/LICENSE create mode 100644 lib/symfony/property-info/PhpStan/NameScope.php create mode 100644 lib/symfony/property-info/PhpStan/NameScopeFactory.php create mode 100644 lib/symfony/property-info/PropertyAccessExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyDescriptionExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyInfoCacheExtractor.php create mode 100644 lib/symfony/property-info/PropertyInfoExtractor.php create mode 100644 lib/symfony/property-info/PropertyInfoExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyInitializableExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyListExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyReadInfo.php create mode 100644 lib/symfony/property-info/PropertyReadInfoExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyTypeExtractorInterface.php create mode 100644 lib/symfony/property-info/PropertyWriteInfo.php create mode 100644 lib/symfony/property-info/PropertyWriteInfoExtractorInterface.php create mode 100644 lib/symfony/property-info/README.md create mode 100644 lib/symfony/property-info/Type.php create mode 100644 lib/symfony/property-info/Util/PhpDocTypeHelper.php create mode 100644 lib/symfony/property-info/Util/PhpStanTypeHelper.php create mode 100644 lib/symfony/property-info/composer.json diff --git a/lib/symfony/form/AbstractExtension.php b/lib/symfony/form/AbstractExtension.php new file mode 100644 index 0000000000..0d846e97b5 --- /dev/null +++ b/lib/symfony/form/AbstractExtension.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractExtension implements FormExtensionInterface +{ + /** + * The types provided by this extension. + * + * @var FormTypeInterface[] + */ + private array $types; + + /** + * The type extensions provided by this extension. + * + * @var FormTypeExtensionInterface[][] + */ + private array $typeExtensions; + + /** + * The type guesser provided by this extension. + */ + private ?FormTypeGuesserInterface $typeGuesser = null; + + /** + * Whether the type guesser has been loaded. + */ + private bool $typeGuesserLoaded = false; + + public function getType(string $name): FormTypeInterface + { + if (!isset($this->types)) { + $this->initTypes(); + } + + if (!isset($this->types[$name])) { + throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name)); + } + + return $this->types[$name]; + } + + public function hasType(string $name): bool + { + if (!isset($this->types)) { + $this->initTypes(); + } + + return isset($this->types[$name]); + } + + public function getTypeExtensions(string $name): array + { + if (!isset($this->typeExtensions)) { + $this->initTypeExtensions(); + } + + return $this->typeExtensions[$name] + ?? []; + } + + public function hasTypeExtensions(string $name): bool + { + if (!isset($this->typeExtensions)) { + $this->initTypeExtensions(); + } + + return isset($this->typeExtensions[$name]) && \count($this->typeExtensions[$name]) > 0; + } + + public function getTypeGuesser(): ?FormTypeGuesserInterface + { + if (!$this->typeGuesserLoaded) { + $this->initTypeGuesser(); + } + + return $this->typeGuesser; + } + + /** + * Registers the types. + * + * @return FormTypeInterface[] + */ + protected function loadTypes() + { + return []; + } + + /** + * Registers the type extensions. + * + * @return FormTypeExtensionInterface[] + */ + protected function loadTypeExtensions(): array + { + return []; + } + + /** + * Registers the type guesser. + * + * @return FormTypeGuesserInterface|null + */ + protected function loadTypeGuesser() + { + return null; + } + + /** + * Initializes the types. + * + * @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface + */ + private function initTypes(): void + { + $this->types = []; + + foreach ($this->loadTypes() as $type) { + if (!$type instanceof FormTypeInterface) { + throw new UnexpectedTypeException($type, FormTypeInterface::class); + } + + $this->types[$type::class] = $type; + } + } + + /** + * Initializes the type extensions. + * + * @throws UnexpectedTypeException if any registered type extension is not + * an instance of FormTypeExtensionInterface + */ + private function initTypeExtensions(): void + { + $this->typeExtensions = []; + + foreach ($this->loadTypeExtensions() as $extension) { + if (!$extension instanceof FormTypeExtensionInterface) { + throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class); + } + + foreach ($extension::getExtendedTypes() as $extendedType) { + $this->typeExtensions[$extendedType][] = $extension; + } + } + } + + /** + * Initializes the type guesser. + * + * @throws UnexpectedTypeException if the type guesser is not an instance of FormTypeGuesserInterface + */ + private function initTypeGuesser(): void + { + $this->typeGuesserLoaded = true; + + $this->typeGuesser = $this->loadTypeGuesser(); + if (null !== $this->typeGuesser && !$this->typeGuesser instanceof FormTypeGuesserInterface) { + throw new UnexpectedTypeException($this->typeGuesser, FormTypeGuesserInterface::class); + } + } +} diff --git a/lib/symfony/form/AbstractRendererEngine.php b/lib/symfony/form/AbstractRendererEngine.php new file mode 100644 index 0000000000..3f1ab79c26 --- /dev/null +++ b/lib/symfony/form/AbstractRendererEngine.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Contracts\Service\ResetInterface; + +/** + * Default implementation of {@link FormRendererEngineInterface}. + * + * @author Bernhard Schussek + */ +abstract class AbstractRendererEngine implements FormRendererEngineInterface, ResetInterface +{ + /** + * The variable in {@link FormView} used as cache key. + */ + public const CACHE_KEY_VAR = 'cache_key'; + + /** + * @var array + */ + protected $defaultThemes; + + /** + * @var array[] + */ + protected $themes = []; + + /** + * @var bool[] + */ + protected $useDefaultThemes = []; + + /** + * @var array[] + */ + protected $resources = []; + + /** + * @var array> + */ + private array $resourceHierarchyLevels = []; + + /** + * Creates a new renderer engine. + * + * @param array $defaultThemes The default themes. The type of these + * themes is open to the implementation. + */ + public function __construct(array $defaultThemes = []) + { + $this->defaultThemes = $defaultThemes; + } + + /** + * @return void + */ + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true) + { + $cacheKey = $view->vars[self::CACHE_KEY_VAR]; + + // Do not cast, as casting turns objects into arrays of properties + $this->themes[$cacheKey] = \is_array($themes) ? $themes : [$themes]; + $this->useDefaultThemes[$cacheKey] = $useDefaultThemes; + + // Unset instead of resetting to an empty array, in order to allow + // implementations (like TwigRendererEngine) to check whether $cacheKey + // is set at all. + unset($this->resources[$cacheKey], $this->resourceHierarchyLevels[$cacheKey]); + } + + public function getResourceForBlockName(FormView $view, string $blockName): mixed + { + $cacheKey = $view->vars[self::CACHE_KEY_VAR]; + + if (!isset($this->resources[$cacheKey][$blockName])) { + $this->loadResourceForBlockName($cacheKey, $view, $blockName); + } + + return $this->resources[$cacheKey][$blockName]; + } + + public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed + { + $cacheKey = $view->vars[self::CACHE_KEY_VAR]; + $blockName = $blockNameHierarchy[$hierarchyLevel]; + + if (!isset($this->resources[$cacheKey][$blockName])) { + $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel); + } + + return $this->resources[$cacheKey][$blockName]; + } + + public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false + { + $cacheKey = $view->vars[self::CACHE_KEY_VAR]; + $blockName = $blockNameHierarchy[$hierarchyLevel]; + + if (!isset($this->resources[$cacheKey][$blockName])) { + $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel); + } + + // If $block was previously rendered loaded with loadTemplateForBlock(), the template + // is cached but the hierarchy level is not. In this case, we know that the block + // exists at this very hierarchy level, so we can just set it. + if (!isset($this->resourceHierarchyLevels[$cacheKey][$blockName])) { + $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel; + } + + return $this->resourceHierarchyLevels[$cacheKey][$blockName]; + } + + /** + * Loads the cache with the resource for a given block name. + * + * @see getResourceForBlock() + * + * @return bool + */ + abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName); + + /** + * Loads the cache with the resource for a specific level of a block hierarchy. + * + * @see getResourceForBlockHierarchy() + */ + private function loadResourceForBlockNameHierarchy(string $cacheKey, FormView $view, array $blockNameHierarchy, int $hierarchyLevel): bool + { + $blockName = $blockNameHierarchy[$hierarchyLevel]; + + // Try to find a template for that block + if ($this->loadResourceForBlockName($cacheKey, $view, $blockName)) { + // If loadTemplateForBlock() returns true, it was able to populate the + // cache. The only missing thing is to set the hierarchy level at which + // the template was found. + $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel; + + return true; + } + + if ($hierarchyLevel > 0) { + $parentLevel = $hierarchyLevel - 1; + $parentBlockName = $blockNameHierarchy[$parentLevel]; + + // The next two if statements contain slightly duplicated code. This is by intention + // and tries to avoid execution of unnecessary checks in order to increase performance. + + if (isset($this->resources[$cacheKey][$parentBlockName])) { + // It may happen that the parent block is already loaded, but its level is not. + // In this case, the parent block must have been loaded by loadResourceForBlock(), + // which does not check the hierarchy of the block. Subsequently the block must have + // been found directly on the parent level. + if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlockName])) { + $this->resourceHierarchyLevels[$cacheKey][$parentBlockName] = $parentLevel; + } + + // Cache the shortcuts for further accesses + $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName]; + $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName]; + + return true; + } + + if ($this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $parentLevel)) { + // Cache the shortcuts for further accesses + $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName]; + $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName]; + + return true; + } + } + + // Cache the result for further accesses + $this->resources[$cacheKey][$blockName] = false; + $this->resourceHierarchyLevels[$cacheKey][$blockName] = false; + + return false; + } + + public function reset(): void + { + $this->themes = []; + $this->useDefaultThemes = []; + $this->resources = []; + $this->resourceHierarchyLevels = []; + } +} diff --git a/lib/symfony/form/AbstractType.php b/lib/symfony/form/AbstractType.php new file mode 100644 index 0000000000..8fffa379d8 --- /dev/null +++ b/lib/symfony/form/AbstractType.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Util\StringUtil; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractType implements FormTypeInterface +{ + /** + * @return string|null + */ + public function getParent() + { + return FormType::class; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return StringUtil::fqcnToBlockPrefix(static::class) ?: ''; + } +} diff --git a/lib/symfony/form/AbstractTypeExtension.php b/lib/symfony/form/AbstractTypeExtension.php new file mode 100644 index 0000000000..1956bd00a7 --- /dev/null +++ b/lib/symfony/form/AbstractTypeExtension.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractTypeExtension implements FormTypeExtensionInterface +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + } +} diff --git a/lib/symfony/form/Button.php b/lib/symfony/form/Button.php new file mode 100644 index 0000000000..fac7b444c3 --- /dev/null +++ b/lib/symfony/form/Button.php @@ -0,0 +1,373 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\AlreadySubmittedException; +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * A form button. + * + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class Button implements \IteratorAggregate, FormInterface +{ + private ?FormInterface $parent = null; + private FormConfigInterface $config; + private bool $submitted = false; + + /** + * Creates a new button from a form configuration. + */ + public function __construct(FormConfigInterface $config) + { + $this->config = $config; + } + + /** + * Unsupported method. + */ + public function offsetExists(mixed $offset): bool + { + return false; + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @throws BadMethodCallException + */ + public function offsetGet(mixed $offset): FormInterface + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @throws BadMethodCallException + */ + public function offsetSet(mixed $offset, mixed $value): void + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @throws BadMethodCallException + */ + public function offsetUnset(mixed $offset): void + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + public function setParent(?FormInterface $parent = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/form', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if ($this->submitted) { + throw new AlreadySubmittedException('You cannot set the parent of a submitted button.'); + } + + $this->parent = $parent; + + return $this; + } + + public function getParent(): ?FormInterface + { + return $this->parent; + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @throws BadMethodCallException + */ + public function add(string|FormInterface $child, ?string $type = null, array $options = []): static + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @throws BadMethodCallException + */ + public function get(string $name): FormInterface + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + */ + public function has(string $name): bool + { + return false; + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @throws BadMethodCallException + */ + public function remove(string $name): static + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + public function all(): array + { + return []; + } + + public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator + { + return new FormErrorIterator($this, []); + } + + /** + * Unsupported method. + * + * This method should not be invoked. + * + * @return $this + */ + public function setData(mixed $modelData): static + { + // no-op, called during initialization of the form tree + return $this; + } + + /** + * Unsupported method. + */ + public function getData(): mixed + { + return null; + } + + /** + * Unsupported method. + */ + public function getNormData(): mixed + { + return null; + } + + /** + * Unsupported method. + */ + public function getViewData(): mixed + { + return null; + } + + /** + * Unsupported method. + */ + public function getExtraData(): array + { + return []; + } + + /** + * Returns the button's configuration. + */ + public function getConfig(): FormConfigInterface + { + return $this->config; + } + + /** + * Returns whether the button is submitted. + */ + public function isSubmitted(): bool + { + return $this->submitted; + } + + /** + * Returns the name by which the button is identified in forms. + */ + public function getName(): string + { + return $this->config->getName(); + } + + /** + * Unsupported method. + */ + public function getPropertyPath(): ?PropertyPathInterface + { + return null; + } + + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function addError(FormError $error): static + { + throw new BadMethodCallException('Buttons cannot have errors.'); + } + + /** + * Unsupported method. + */ + public function isValid(): bool + { + return true; + } + + /** + * Unsupported method. + */ + public function isRequired(): bool + { + return false; + } + + public function isDisabled(): bool + { + if ($this->parent?->isDisabled()) { + return true; + } + + return $this->config->getDisabled(); + } + + /** + * Unsupported method. + */ + public function isEmpty(): bool + { + return true; + } + + /** + * Unsupported method. + */ + public function isSynchronized(): bool + { + return true; + } + + /** + * Unsupported method. + */ + public function getTransformationFailure(): ?TransformationFailedException + { + return null; + } + + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function initialize(): static + { + throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.'); + } + + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function handleRequest(mixed $request = null): static + { + throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.'); + } + + /** + * Submits data to the button. + * + * @return $this + * + * @throws AlreadySubmittedException if the button has already been submitted + */ + public function submit(array|string|null $submittedData, bool $clearMissing = true): static + { + if ($this->submitted) { + throw new AlreadySubmittedException('A form can only be submitted once.'); + } + + $this->submitted = true; + + return $this; + } + + public function getRoot(): FormInterface + { + return $this->parent ? $this->parent->getRoot() : $this; + } + + public function isRoot(): bool + { + return null === $this->parent; + } + + public function createView(?FormView $parent = null): FormView + { + if (null === $parent && $this->parent) { + $parent = $this->parent->createView(); + } + + $type = $this->config->getType(); + $options = $this->config->getOptions(); + + $view = $type->createView($this, $parent); + + $type->buildView($view, $this, $options); + $type->finishView($view, $this, $options); + + return $view; + } + + /** + * Unsupported method. + */ + public function count(): int + { + return 0; + } + + /** + * Unsupported method. + */ + public function getIterator(): \EmptyIterator + { + return new \EmptyIterator(); + } +} diff --git a/lib/symfony/form/ButtonBuilder.php b/lib/symfony/form/ButtonBuilder.php new file mode 100644 index 0000000000..626920ee55 --- /dev/null +++ b/lib/symfony/form/ButtonBuilder.php @@ -0,0 +1,738 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * A builder for {@link Button} instances. + * + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface +{ + protected $locked = false; + + private bool $disabled = false; + private ResolvedFormTypeInterface $type; + private string $name; + private array $attributes = []; + private array $options; + + /** + * @throws InvalidArgumentException if the name is empty + */ + public function __construct(?string $name, array $options = []) + { + if ('' === $name || null === $name) { + throw new InvalidArgumentException('Buttons cannot have empty names.'); + } + + $this->name = $name; + $this->options = $options; + + FormConfigBuilder::validateName($name); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function add(string|FormBuilderInterface $child, ?string $type = null, array $options = []): static + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function get(string $name): FormBuilderInterface + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function remove(string $name): static + { + throw new BadMethodCallException('Buttons cannot have children.'); + } + + /** + * Unsupported method. + */ + public function has(string $name): bool + { + return false; + } + + /** + * Returns the children. + */ + public function all(): array + { + return []; + } + + /** + * Creates the button. + */ + public function getForm(): Button + { + return new Button($this->getFormConfig()); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static + { + throw new BadMethodCallException('Buttons do not support event listeners.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function addEventSubscriber(EventSubscriberInterface $subscriber): static + { + throw new BadMethodCallException('Buttons do not support event subscribers.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static + { + throw new BadMethodCallException('Buttons do not support data transformers.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function resetViewTransformers(): static + { + throw new BadMethodCallException('Buttons do not support data transformers.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static + { + throw new BadMethodCallException('Buttons do not support data transformers.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function resetModelTransformers(): static + { + throw new BadMethodCallException('Buttons do not support data transformers.'); + } + + /** + * @return $this + */ + public function setAttribute(string $name, mixed $value): static + { + $this->attributes[$name] = $value; + + return $this; + } + + /** + * @return $this + */ + public function setAttributes(array $attributes): static + { + $this->attributes = $attributes; + + return $this; + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setDataMapper(?DataMapperInterface $dataMapper = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/form', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + + throw new BadMethodCallException('Buttons do not support data mappers.'); + } + + /** + * Set whether the button is disabled. + * + * @return $this + */ + public function setDisabled(bool $disabled): static + { + $this->disabled = $disabled; + + return $this; + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setEmptyData(mixed $emptyData): static + { + throw new BadMethodCallException('Buttons do not support empty data.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setErrorBubbling(bool $errorBubbling): static + { + throw new BadMethodCallException('Buttons do not support error bubbling.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setRequired(bool $required): static + { + throw new BadMethodCallException('Buttons cannot be required.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static + { + throw new BadMethodCallException('Buttons do not support property paths.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setMapped(bool $mapped): static + { + throw new BadMethodCallException('Buttons do not support data mapping.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setByReference(bool $byReference): static + { + throw new BadMethodCallException('Buttons do not support data mapping.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setCompound(bool $compound): static + { + throw new BadMethodCallException('Buttons cannot be compound.'); + } + + /** + * Sets the type of the button. + * + * @return $this + */ + public function setType(ResolvedFormTypeInterface $type): static + { + $this->type = $type; + + return $this; + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setData(mixed $data): static + { + throw new BadMethodCallException('Buttons do not support data.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setDataLocked(bool $locked): static + { + throw new BadMethodCallException('Buttons do not support data locking.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setFormFactory(FormFactoryInterface $formFactory) + { + throw new BadMethodCallException('Buttons do not support form factories.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setAction(string $action): static + { + throw new BadMethodCallException('Buttons do not support actions.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setMethod(string $method): static + { + throw new BadMethodCallException('Buttons do not support methods.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setRequestHandler(RequestHandlerInterface $requestHandler): static + { + throw new BadMethodCallException('Buttons do not support request handlers.'); + } + + /** + * Unsupported method. + * + * @return $this + * + * @throws BadMethodCallException + */ + public function setAutoInitialize(bool $initialize): static + { + if (true === $initialize) { + throw new BadMethodCallException('Buttons do not support automatic initialization.'); + } + + return $this; + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setInheritData(bool $inheritData): static + { + throw new BadMethodCallException('Buttons do not support data inheritance.'); + } + + /** + * Builds and returns the button configuration. + */ + public function getFormConfig(): FormConfigInterface + { + // This method should be idempotent, so clone the builder + $config = clone $this; + $config->locked = true; + + return $config; + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function setIsEmptyCallback(?callable $isEmptyCallback): static + { + throw new BadMethodCallException('Buttons do not support "is empty" callback.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function getEventDispatcher(): EventDispatcherInterface + { + throw new BadMethodCallException('Buttons do not support event dispatching.'); + } + + public function getName(): string + { + return $this->name; + } + + /** + * Unsupported method. + */ + public function getPropertyPath(): ?PropertyPathInterface + { + return null; + } + + /** + * Unsupported method. + */ + public function getMapped(): bool + { + return false; + } + + /** + * Unsupported method. + */ + public function getByReference(): bool + { + return false; + } + + /** + * Unsupported method. + */ + public function getCompound(): bool + { + return false; + } + + /** + * Returns the form type used to construct the button. + */ + public function getType(): ResolvedFormTypeInterface + { + return $this->type; + } + + /** + * Unsupported method. + */ + public function getViewTransformers(): array + { + return []; + } + + /** + * Unsupported method. + */ + public function getModelTransformers(): array + { + return []; + } + + /** + * Unsupported method. + */ + public function getDataMapper(): ?DataMapperInterface + { + return null; + } + + /** + * Unsupported method. + */ + public function getRequired(): bool + { + return false; + } + + /** + * Returns whether the button is disabled. + */ + public function getDisabled(): bool + { + return $this->disabled; + } + + /** + * Unsupported method. + */ + public function getErrorBubbling(): bool + { + return false; + } + + /** + * Unsupported method. + */ + public function getEmptyData(): mixed + { + return null; + } + + /** + * Returns additional attributes of the button. + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * Returns whether the attribute with the given name exists. + */ + public function hasAttribute(string $name): bool + { + return \array_key_exists($name, $this->attributes); + } + + /** + * Returns the value of the given attribute. + */ + public function getAttribute(string $name, mixed $default = null): mixed + { + return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + /** + * Unsupported method. + */ + public function getData(): mixed + { + return null; + } + + /** + * Unsupported method. + */ + public function getDataClass(): ?string + { + return null; + } + + /** + * Unsupported method. + */ + public function getDataLocked(): bool + { + return false; + } + + /** + * Unsupported method. + * + * @return never + */ + public function getFormFactory(): FormFactoryInterface + { + throw new BadMethodCallException('Buttons do not support adding children.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function getAction(): string + { + throw new BadMethodCallException('Buttons do not support actions.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function getMethod(): string + { + throw new BadMethodCallException('Buttons do not support methods.'); + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function getRequestHandler(): RequestHandlerInterface + { + throw new BadMethodCallException('Buttons do not support request handlers.'); + } + + /** + * Unsupported method. + */ + public function getAutoInitialize(): bool + { + return false; + } + + /** + * Unsupported method. + */ + public function getInheritData(): bool + { + return false; + } + + /** + * Returns all options passed during the construction of the button. + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Returns whether a specific option exists. + */ + public function hasOption(string $name): bool + { + return \array_key_exists($name, $this->options); + } + + /** + * Returns the value of a specific option. + */ + public function getOption(string $name, mixed $default = null): mixed + { + return \array_key_exists($name, $this->options) ? $this->options[$name] : $default; + } + + /** + * Unsupported method. + * + * @return never + * + * @throws BadMethodCallException + */ + public function getIsEmptyCallback(): ?callable + { + throw new BadMethodCallException('Buttons do not support "is empty" callback.'); + } + + /** + * Unsupported method. + */ + public function count(): int + { + return 0; + } + + /** + * Unsupported method. + */ + public function getIterator(): \EmptyIterator + { + return new \EmptyIterator(); + } +} diff --git a/lib/symfony/form/ButtonTypeInterface.php b/lib/symfony/form/ButtonTypeInterface.php new file mode 100644 index 0000000000..dd5117c4da --- /dev/null +++ b/lib/symfony/form/ButtonTypeInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * A type that should be converted into a {@link Button} instance. + * + * @author Bernhard Schussek + */ +interface ButtonTypeInterface extends FormTypeInterface +{ +} diff --git a/lib/symfony/form/CHANGELOG.md b/lib/symfony/form/CHANGELOG.md new file mode 100644 index 0000000000..9fba1a3f5a --- /dev/null +++ b/lib/symfony/form/CHANGELOG.md @@ -0,0 +1,616 @@ +CHANGELOG +========= + +6.4 +--- + + * Deprecate using `DateTime` or `DateTimeImmutable` model data with a different timezone than configured with the + `model_timezone` option in `DateType`, `DateTimeType`, and `TimeType` + * Deprecate `PostSetDataEvent::setData()`, use `PreSetDataEvent::setData()` instead + * Deprecate `PostSubmitEvent::setData()`, use `PreSubmitDataEvent::setData()` or `SubmitDataEvent::setData()` instead + * Add `duplicate_preferred_choices` option in `ChoiceType` + * Add `$duplicatePreferredChoices` parameter to `ChoiceListFactoryInterface::createView()` + +6.3 +--- + + * Don't render seconds for HTML5 date pickers unless "with_seconds" is explicitly set + * Add a `placeholder_attr` option to `ChoiceType` + * Deprecate not configuring the "widget" option of date/time form types, it will default to "single_text" in v7 + +6.2 +--- + + * Allow passing `TranslatableInterface` objects to the `ChoiceView` label + * Allow passing `TranslatableInterface` objects to the `help` option + * Deprecate calling `Button/Form::setParent()`, `ButtonBuilder/FormConfigBuilder::setDataMapper()`, `TransformationFailedException::setInvalidMessage()` without arguments + * Change the signature of `FormConfigBuilderInterface::setDataMapper()` to `setDataMapper(?DataMapperInterface)` + * Change the signature of `FormInterface::setParent()` to `setParent(?self)` + * Add `PasswordHasherExtension` with support for `hash_property_path` option in `PasswordType` + +6.1 +--- + + * Add a `prototype_options` option to `CollectionType` + +6.0 +--- + + * Remove `PropertyPathMaper` + * Remove `Symfony\Component\Form\Extension\Validator\Util\ServerParams` + * Remove `FormPass` configuration + * Remove the `NumberToLocalizedStringTransformer::ROUND_*` constants, use `\NumberFormatter::ROUND_*` instead + * The `rounding_mode` option of the `PercentType` defaults to `\NumberFormatter::ROUND_HALFUP` + * The rounding mode argument of the constructor of `PercentToLocalizedStringTransformer` defaults to `\NumberFormatter::ROUND_HALFUP` + * Add `FormConfigInterface::getIsEmptyCallback()` and `FormConfigBuilderInterface::setIsEmptyCallback()` + * Change `$forms` parameter type of the `DataMapper::mapDataToForms()` method from `iterable` to `\Traversable` + * Change `$forms` parameter type of the `DataMapper::mapFormsToData()` method from `iterable` to `\Traversable` + * Change `$checkboxes` parameter type of the `CheckboxListMapper::mapDataToForms()` method from `iterable` to `\Traversable` + * Change `$checkboxes` parameter type of the `CheckboxListMapper::mapFormsToData()` method from `iterable` to `\Traversable` + * Change `$radios` parameter type of the `RadioListMapper::mapDataToForms()` method from `iterable` to `\Traversable` + * Change `$radios` parameter type of the `RadioListMapper::mapFormsToData()` method from `iterable` to `\Traversable` + +5.4 +--- + + * Deprecate calling `FormErrorIterator::children()` if the current element is not iterable. + * Allow to pass `TranslatableMessage` objects to the `help` option + * Add the `EnumType` + +5.3 +--- + + * Changed `$forms` parameter type of the `DataMapperInterface::mapDataToForms()` method from `iterable` to `\Traversable`. + * Changed `$forms` parameter type of the `DataMapperInterface::mapFormsToData()` method from `iterable` to `\Traversable`. + * Deprecated passing an array as the second argument of the `DataMapper::mapDataToForms()` method, pass `\Traversable` instead. + * Deprecated passing an array as the first argument of the `DataMapper::mapFormsToData()` method, pass `\Traversable` instead. + * Deprecated passing an array as the second argument of the `CheckboxListMapper::mapDataToForms()` method, pass `\Traversable` instead. + * Deprecated passing an array as the first argument of the `CheckboxListMapper::mapFormsToData()` method, pass `\Traversable` instead. + * Deprecated passing an array as the second argument of the `RadioListMapper::mapDataToForms()` method, pass `\Traversable` instead. + * Deprecated passing an array as the first argument of the `RadioListMapper::mapFormsToData()` method, pass `\Traversable` instead. + * Added a `choice_translation_parameters` option to `ChoiceType` + * Add `UuidType` and `UlidType` + * Dependency on `symfony/intl` was removed. Install `symfony/intl` if you are using `LocaleType`, `CountryType`, `CurrencyType`, `LanguageType` or `TimezoneType`. + * Add `priority` option to `BaseType` and sorting view fields + +5.2.0 +----- + + * Added support for using the `{{ label }}` placeholder in constraint messages, which is replaced in the `ViolationMapper` by the corresponding field form label. + * Added `DataMapper`, `ChainAccessor`, `PropertyPathAccessor` and `CallbackAccessor` with new callable `getter` and `setter` options for each form type + * Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor` + * Added an `html5` option to `MoneyType` and `PercentType`, to use `` + +5.1.0 +----- + + * Deprecated not configuring the `rounding_mode` option of the `PercentType`. It will default to `\NumberFormatter::ROUND_HALFUP` in Symfony 6. + * Deprecated not passing a rounding mode to the constructor of `PercentToLocalizedStringTransformer`. It will default to `\NumberFormatter::ROUND_HALFUP` in Symfony 6. + * Added `collection_entry` block prefix to `CollectionType` entries + * Added a `choice_filter` option to `ChoiceType` + * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()` - not defining them is deprecated. + * Added a `ChoiceList` facade to leverage explicit choice list caching based on options + * Added an `AbstractChoiceLoader` to simplify implementations and handle global optimizations + * The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured. + * Implementing the `FormConfigInterface` without implementing the `getIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + * Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + * Added a `rounding_mode` option for the PercentType and correctly round the value when submitted + * Deprecated `Symfony\Component\Form\Extension\Validator\Util\ServerParams` in favor of its parent class `Symfony\Component\Form\Util\ServerParams` + * Added the `html5` option to the `ColorType` to validate the input + * Deprecated `NumberToLocalizedStringTransformer::ROUND_*` constants, use `\NumberFormatter::ROUND_*` instead + +5.0.0 +----- + + * Removed support for using different values for the "model_timezone" and "view_timezone" options of the `TimeType` + without configuring a reference date. + * Removed the `scale` option of the `IntegerType`. + * Using the `date_format`, `date_widget`, and `time_widget` options of the `DateTimeType` when the `widget` option is + set to `single_text` is not supported anymore. + * The `format` option of `DateType` and `DateTimeType` cannot be used when the `html5` option is enabled. + * Using names for buttons that do not start with a letter, a digit, or an underscore throw an exception + * Using names for buttons that do not contain only letters, digits, underscores, hyphens, and colons throw an exception. + * removed the `ChoiceLoaderInterface` implementation in `CountryType`, `LanguageType`, `LocaleType` and `CurrencyType` + * removed `getExtendedType()` method of the `FormTypeExtensionInterface` + * added static `getExtendedTypes()` method to the `FormTypeExtensionInterface` + * calling to `FormRenderer::searchAndRenderBlock()` method for fields which were already rendered throw a `BadMethodCallException` + * removed the `regions` option of the `TimezoneType` + * removed the `$scale` argument of the `IntegerToLocalizedStringTransformer` + * removed `TemplatingExtension` and `TemplatingRendererEngine` classes, use Twig instead + * passing a null message when instantiating a `Symfony\Component\Form\FormError` is not allowed + * removed support for using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` + +4.4.0 +----- + + * add new `WeekType` + * using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a + reference date is deprecated + * preferred choices are repeated in the list of all choices + * deprecated using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` + * The type guesser guesses the HTML accept attribute when a mime type is configured in the File or Image constraint. + * Overriding the methods `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` without the `void` return-type is deprecated. + * marked all dispatched event classes as `@final` + * Added the `validate` option to `SubmitType` to toggle the browser built-in form validation. + * Added the `alpha3` option to `LanguageType` and `CountryType` to use alpha3 instead of alpha2 codes + +4.3.0 +----- + + * added a `symbol` option to the `PercentType` that allows to disable or customize the output of the percent character + * Using the `format` option of `DateType` and `DateTimeType` when the `html5` option is enabled is deprecated. + * Using names for buttons that do not start with a letter, a digit, or an underscore is deprecated and will lead to an + exception in 5.0. + * Using names for buttons that do not contain only letters, digits, underscores, hyphens, and colons is deprecated and + will lead to an exception in 5.0. + * added `html5` option to `NumberType` that allows to render `type="number"` input fields + * deprecated using the `date_format`, `date_widget`, and `time_widget` options of the `DateTimeType` when the `widget` + option is set to `single_text` + * added `block_prefix` option to `BaseType`. + * added `help_html` option to display the `help` text as HTML. + * `FormError` doesn't implement `Serializable` anymore + * `FormDataCollector` has been marked as `final` + * added `label_translation_parameters`, `attr_translation_parameters`, `help_translation_parameters` options + to `FormType` to pass translation parameters to form labels, attributes (`placeholder` and `title`) and help text respectively. + The passed parameters will replace placeholders in translation messages. + + ```php + class OrderType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('comment', TextType::class, [ + 'label' => 'Comment to the order to %company%', + 'label_translation_parameters' => [ + '%company%' => 'Acme', + ], + 'help' => 'The address of the %company% is %address%', + 'help_translation_parameters' => [ + '%company%' => 'Acme Ltd.', + '%address%' => '4 Form street, Symfonyville', + ], + ]) + } + } + ``` + * added the `input_format` option to `DateType`, `DateTimeType`, and `TimeType` to specify the input format when setting + the `input` option to `string` + * dispatch `PreSubmitEvent` on `form.pre_submit` + * dispatch `SubmitEvent` on `form.submit` + * dispatch `PostSubmitEvent` on `form.post_submit` + * dispatch `PreSetDataEvent` on `form.pre_set_data` + * dispatch `PostSetDataEvent` on `form.post_set_data` + * added an `input` option to `NumberType` + * removed default option grouping in `TimezoneType`, use `group_by` instead + +4.2.0 +----- + + * The `getExtendedType()` method of the `FormTypeExtensionInterface` is deprecated and will be removed in 5.0. Type + extensions must implement the static `getExtendedTypes()` method instead and return an iterable of extended types. + + Before: + + ```php + class FooTypeExtension extends AbstractTypeExtension + { + public function getExtendedType() + { + return FormType::class; + } + + // ... + } + ``` + + After: + + ```php + class FooTypeExtension extends AbstractTypeExtension + { + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } + + // ... + } + ``` + * deprecated the `$scale` argument of the `IntegerToLocalizedStringTransformer` + * added `Symfony\Component\Form\ClearableErrorsInterface` + * deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered + * added a cause when a CSRF error has occurred + * deprecated the `scale` option of the `IntegerType` + * removed restriction on allowed HTTP methods + * deprecated the `regions` option of the `TimezoneType` + +4.1.0 +----- + + * added `input=datetime_immutable` to `DateType`, `TimeType`, `DateTimeType` + * added `rounding_mode` option to `MoneyType` + * added `choice_translation_locale` option to `CountryType`, `LanguageType`, `LocaleType` and `CurrencyType` + * deprecated the `ChoiceLoaderInterface` implementation in `CountryType`, `LanguageType`, `LocaleType` and `CurrencyType` + * added `input=datetime_immutable` to DateType, TimeType, DateTimeType + * added `rounding_mode` option to MoneyType + +4.0.0 +----- + + * using the `choices` option in `CountryType`, `CurrencyType`, `LanguageType`, + `LocaleType`, and `TimezoneType` when the `choice_loader` option is not `null` + is not supported anymore and the configured choices will be ignored + * callable strings that are passed to the options of the `ChoiceType` are + treated as property paths + * the `choices_as_values` option of the `ChoiceType` has been removed + * removed the support for caching loaded choice lists in `LazyChoiceList`, + cache the choice list in the used `ChoiceLoaderInterface` implementation + instead + * removed the support for objects implementing both `\Traversable` and `\ArrayAccess` in `ResizeFormListener::preSubmit()` + * removed the ability to use `FormDataCollector` without the `symfony/var-dumper` component + * removed passing a `ValueExporter` instance to the `FormDataExtractor::__construct()` method + * removed passing guesser services ids as the fourth argument of `DependencyInjectionExtension::__construct()` + * removed the ability to validate an unsubmitted form. + * removed `ChoiceLoaderInterface` implementation in `TimezoneType` + * added the `false_values` option to the `CheckboxType` which allows to configure custom values which will be treated as `false` during submission + +3.4.0 +----- + + * added `DebugCommand` + * deprecated `ChoiceLoaderInterface` implementation in `TimezoneType` + * added options "input" and "regions" to `TimezoneType` + * added an option to ``Symfony\Component\Form\FormRendererEngineInterface::setTheme()`` and + ``Symfony\Component\Form\FormRendererInterface::setTheme()`` to disable usage of default themes when rendering a form + +3.3.0 +----- + + * deprecated using "choices" option in ``CountryType``, ``CurrencyType``, ``LanguageType``, ``LocaleType``, and + ``TimezoneType`` when "choice_loader" is not ``null`` + * added `Symfony\Component\Form\FormErrorIterator::findByCodes()` + * added `getTypedExtensions`, `getTypes`, and `getTypeGuessers` to `Symfony\Component\Form\Test\FormIntegrationTestCase` + * added `FormPass` + +3.2.0 +----- + + * added `CallbackChoiceLoader` + * implemented `ChoiceLoaderInterface` in children of `ChoiceType` + +3.1.0 +----- + + * deprecated the "choices_as_values" option of ChoiceType + * deprecated support for data objects that implements both `Traversable` and + `ArrayAccess` in `ResizeFormListener::preSubmit` method + * Using callable strings as choice options in `ChoiceType` has been deprecated + and will be used as `PropertyPath` instead of callable in Symfony 4.0. + * implemented `DataTransformerInterface` in `TextType` + * deprecated caching loaded choice list in `LazyChoiceList::$loadedList` + +3.0.0 +----- + + * removed `FormTypeInterface::setDefaultOptions()` method + * removed `AbstractType::setDefaultOptions()` method + * removed `FormTypeExtensionInterface::setDefaultOptions()` method + * removed `AbstractTypeExtension::setDefaultOptions()` method + * added `FormTypeInterface::configureOptions()` method + * added `FormTypeExtensionInterface::configureOptions()` method + +2.8.0 +----- + + * added option "choice_translation_domain" to DateType, TimeType and DateTimeType. + * deprecated option "read_only" in favor of "attr['readonly']" + * added the html5 "range" FormType + * deprecated the "cascade_validation" option in favor of setting "constraints" + with the Valid constraint + * moved data trimming logic of TrimListener into StringUtil + * [BC BREAK] When registering a type extension through the DI extension, the tag alias has to match the actual extended type. + +2.7.38 +------ + + * [BC BREAK] the `isFileUpload()` method was added to the `RequestHandlerInterface` + +2.7.0 +----- + + * added option "choice_translation_domain" to ChoiceType. + * deprecated option "precision" in favor of "scale" + * deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions(). + * deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions(). + * added new ChoiceList interface and implementations in the Symfony\Component\Form\ChoiceList namespace + * added new ChoiceView in the Symfony\Component\Form\ChoiceList\View namespace + * choice groups are now represented by ChoiceGroupView objects in the view + * deprecated the old ChoiceList interface and implementations + * deprecated the old ChoiceView class + * added CheckboxListMapper and RadioListMapper + * deprecated ChoiceToBooleanArrayTransformer and ChoicesToBooleanArrayTransformer + * deprecated FixCheckboxInputListener and FixRadioInputListener + * deprecated the "choice_list" option of ChoiceType + * added new options to ChoiceType: + * "choices_as_values" + * "choice_loader" + * "choice_label" + * "choice_name" + * "choice_value" + * "choice_attr" + * "group_by" + +2.6.2 +----- + + * Added back the `model_timezone` and `view_timezone` options for `TimeType`, `DateType` + and `BirthdayType` + +2.6.0 +----- + + * added "html5" option to Date, Time and DateTimeFormType to be able to + enable/disable HTML5 input date when widget option is "single_text" + * added "label_format" option with possible placeholders "%name%" and "%id%" + * [BC BREAK] drop support for model_timezone and view_timezone options in TimeType, DateType and BirthdayType, + update to 2.6.2 to get back support for these options + +2.5.0 +------ + + * deprecated options "max_length" and "pattern" in favor of putting these values in "attr" option + * added an option for multiple files upload + * form errors now reference their cause (constraint violation, exception, ...) + * form errors now remember which form they were originally added to + * [BC BREAK] added two optional parameters to FormInterface::getErrors() and + changed the method to return a Symfony\Component\Form\FormErrorIterator + instance instead of an array + * errors mapped to unsubmitted forms are discarded now + * ObjectChoiceList now compares choices by their value, if a value path is + given + * you can now pass interface names in the "data_class" option + * [BC BREAK] added `FormInterface::getTransformationFailure()` + +2.4.0 +----- + + * moved CSRF implementation to the new Security CSRF sub-component + * deprecated CsrfProviderInterface and its implementations + * deprecated options "csrf_provider" and "intention" in favor of the new options "csrf_token_manager" and "csrf_token_id" + +2.3.0 +----- + + * deprecated FormPerformanceTestCase and FormIntegrationTestCase in the Symfony\Component\Form\Tests namespace and moved them to the Symfony\Component\Form\Test namespace + * deprecated TypeTestCase in the Symfony\Component\Form\Tests\Extension\Core\Type namespace and moved it to the Symfony\Component\Form\Test namespace + * changed FormRenderer::humanize() to humanize also camel cased field name + * added RequestHandlerInterface and FormInterface::handleRequest() + * deprecated passing a Request instance to FormInterface::bind() + * added options "method" and "action" to FormType + * deprecated option "virtual" in favor "inherit_data" + * deprecated VirtualFormAwareIterator in favor of InheritDataAwareIterator + * [BC BREAK] removed the "array" type hint from DataMapperInterface + * improved forms inheriting their parent data to actually return that data from getData(), getNormData() and getViewData() + * added component-level exceptions for various SPL exceptions + changed all uses of the deprecated Exception class to use more specialized exceptions instead + removed NotInitializedException, NotValidException, TypeDefinitionException, TypeLoaderException, CreationException + * added events PRE_SUBMIT, SUBMIT and POST_SUBMIT + * deprecated events PRE_BIND, BIND and POST_BIND + * [BC BREAK] renamed bind() and isBound() in FormInterface to submit() and isSubmitted() + * added methods submit() and isSubmitted() to Form + * deprecated bind() and isBound() in Form + * deprecated AlreadyBoundException in favor of AlreadySubmittedException + * added support for PATCH requests + * [BC BREAK] added initialize() to FormInterface + * [BC BREAK] added getAutoInitialize() to FormConfigInterface + * [BC BREAK] added setAutoInitialize() to FormConfigBuilderInterface + * [BC BREAK] initialization for Form instances added to a form tree must be manually disabled + * PRE_SET_DATA is now guaranteed to be called after children were added by the form builder, + unless FormInterface::setData() is called manually + * fixed CSRF error message to be translated + * custom CSRF error messages can now be set through the "csrf_message" option + * fixed: expanded single-choice fields now show a radio button for the empty value + +2.2.0 +----- + + * TrimListener now removes unicode whitespaces + * deprecated getParent(), setParent() and hasParent() in FormBuilderInterface + * FormInterface::add() now accepts a FormInterface instance OR a field's name, type and options + * removed special characters between the choice or text fields of DateType unless + the option "format" is set to a custom value + * deprecated FormException and introduced ExceptionInterface instead + * [BC BREAK] FormException is now an interface + * protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig() + * [BC BREAK] inserted argument `$message` in the constructor of `FormError` + * the PropertyPath class and related classes were moved to a dedicated + PropertyAccess component. During the move, InvalidPropertyException was + renamed to NoSuchPropertyException. FormUtil was split: FormUtil::singularify() + can now be found in Symfony\Component\PropertyAccess\StringUtil. The methods + getValue() and setValue() from PropertyPath were extracted into a new class + PropertyAccessor. + * added an optional PropertyAccessorInterface parameter to FormType, + ObjectChoiceList and PropertyPathMapper + * [BC BREAK] PropertyPathMapper and FormType now have a constructor + * [BC BREAK] setting the option "validation_groups" to ``false`` now disables validation + instead of assuming group "Default" + +2.1.0 +----- + + * [BC BREAK] ``read_only`` field attribute now renders as ``readonly="readonly"``, use ``disabled`` instead + * [BC BREAK] child forms now aren't validated anymore by default + * made validation of form children configurable (new option: cascade_validation) + * added support for validation groups as callbacks + * made the translation catalogue configurable via the "translation_domain" option + * added Form::getErrorsAsString() to help debugging forms + * allowed setting different options for RepeatedType fields (like the label) + * added support for empty form name at root level, this enables rendering forms + without form name prefix in field names + * [BC BREAK] form and field names must start with a letter, digit or underscore + and only contain letters, digits, underscores, hyphens and colons + * [BC BREAK] changed default name of the prototype in the "collection" type + from "$$name$$" to "\__name\__". No dollars are appended/prepended to custom + names anymore. + * [BC BREAK] improved ChoiceListInterface + * [BC BREAK] added SimpleChoiceList and LazyChoiceList as replacement of + ArrayChoiceList + * added ChoiceList and ObjectChoiceList to use objects as choices + * [BC BREAK] removed EntitiesToArrayTransformer and EntityToIdTransformer. + The former has been replaced by CollectionToArrayTransformer in combination + with EntityChoiceList, the latter is not required in the core anymore. + * [BC BREAK] renamed + * ArrayToBooleanChoicesTransformer to ChoicesToBooleanArrayTransformer + * ScalarToBooleanChoicesTransformer to ChoiceToBooleanArrayTransformer + * ArrayToChoicesTransformer to ChoicesToValuesTransformer + * ScalarToChoiceTransformer to ChoiceToValueTransformer + to be consistent with the naming in ChoiceListInterface. + They were merged into ChoiceList and have no public equivalent anymore. + * choice fields now throw a FormException if neither the "choices" nor the + "choice_list" option is set + * the radio type is now a child of the checkbox type + * the collection, choice (with multiple selection) and entity (with multiple + selection) types now make use of addXxx() and removeXxx() methods in your + model if you set "by_reference" to false. For a custom, non-recognized + singular form, set the "property_path" option like this: "plural|singular" + * forms now don't create an empty object anymore if they are completely + empty and not required. The empty value for such forms is null. + * added constant Guess::VERY_HIGH_CONFIDENCE + * [BC BREAK] The methods `add`, `remove`, `setParent`, `bind` and `setData` + in class Form now throw an exception if the form is already bound + * fields of constrained classes without a NotBlank or NotNull constraint are + set to not required now, as stated in the docs + * fixed TimeType and DateTimeType to not display seconds when "widget" is + "single_text" unless "with_seconds" is set to true + * checkboxes of in an expanded multiple-choice field don't include the choice + in their name anymore. Their names terminate with "[]" now. + * deprecated FormValidatorInterface and substituted its implementations + by event subscribers + * simplified CSRF protection and removed the csrf type + * deprecated FieldType and merged it into FormType + * added new option "compound" that lets you switch between field and form behavior + * [BC BREAK] renamed theme blocks + * "field_*" to "form_*" + * "field_widget" to "form_widget_simple" + * "widget_choice_options" to "choice_widget_options" + * "generic_label" to "form_label" + * added theme blocks "form_widget_compound", "choice_widget_expanded" and + "choice_widget_collapsed" to make theming more modular + * ValidatorTypeGuesser now guesses "collection" for array type constraint + * added method `guessPattern` to FormTypeGuesserInterface to guess which pattern to use in the HTML5 attribute "pattern" + * deprecated method `guessMinLength` in favor of `guessPattern` + * labels don't display field attributes anymore. Label attributes can be + passed in the "label_attr" option/variable + * added option "mapped" which should be used instead of setting "property_path" to false + * [BC BREAK] "data_class" now *must* be set if a form maps to an object and should be left empty otherwise + * improved error mapping on forms + * dot (".") rules are now allowed to map errors assigned to a form to + one of its children + * errors are not mapped to unsynchronized forms anymore + * [BC BREAK] changed Form constructor to accept a single `FormConfigInterface` object + * [BC BREAK] changed argument order in the FormBuilder constructor + * added Form method `getViewData` + * deprecated Form methods + * `getTypes` + * `getErrorBubbling` + * `getNormTransformers` + * `getClientTransformers` + * `getAttribute` + * `hasAttribute` + * `getClientData` + * added FormBuilder methods + * `getTypes` + * `addViewTransformer` + * `getViewTransformers` + * `resetViewTransformers` + * `addModelTransformer` + * `getModelTransformers` + * `resetModelTransformers` + * deprecated FormBuilder methods + * `prependClientTransformer` + * `appendClientTransformer` + * `getClientTransformers` + * `resetClientTransformers` + * `prependNormTransformer` + * `appendNormTransformer` + * `getNormTransformers` + * `resetNormTransformers` + * deprecated the option "validation_constraint" in favor of the new + option "constraints" + * removed superfluous methods from DataMapperInterface + * `mapFormToData` + * `mapDataToForm` + * added `setDefaultOptions` to FormTypeInterface and FormTypeExtensionInterface + which accepts an OptionsResolverInterface instance + * deprecated the methods `getDefaultOptions` and `getAllowedOptionValues` + in FormTypeInterface and FormTypeExtensionInterface + * options passed during construction can now be accessed from FormConfigInterface + * added FormBuilderInterface and FormConfigEditorInterface + * [BC BREAK] the method `buildForm` in FormTypeInterface and FormTypeExtensionInterface + now receives a FormBuilderInterface instead of a FormBuilder instance + * [BC BREAK] the method `buildViewBottomUp` was renamed to `finishView` in + FormTypeInterface and FormTypeExtensionInterface + * [BC BREAK] the options array is now passed as last argument of the + methods + * `buildView` + * `finishView` + in FormTypeInterface and FormTypeExtensionInterface + * [BC BREAK] no options are passed to `getParent` of FormTypeInterface anymore + * deprecated DataEvent and FilterDataEvent in favor of the new FormEvent which is + now passed to all events thrown by the component + * FormEvents::BIND now replaces FormEvents::BIND_NORM_DATA + * FormEvents::PRE_SET_DATA now replaces FormEvents::SET_DATA + * FormEvents::PRE_BIND now replaces FormEvents::BIND_CLIENT_DATA + * deprecated FormEvents::SET_DATA, FormEvents::BIND_CLIENT_DATA and + FormEvents::BIND_NORM_DATA + * [BC BREAK] reversed the order of the first two arguments to `createNamed` + and `createNamedBuilder` in `FormFactoryInterface` + * deprecated `getChildren` in Form and FormBuilder in favor of `all` + * deprecated `hasChildren` in Form and FormBuilder in favor of `count` + * FormBuilder now implements \IteratorAggregate + * [BC BREAK] compound forms now always need a data mapper + * FormBuilder now maintains the order when explicitly adding form builders as children + * ChoiceType now doesn't add the empty value anymore if the choices already contain an empty element + * DateType, TimeType and DateTimeType now show empty values again if not required + * [BC BREAK] fixed rendering of errors for DateType, BirthdayType and similar ones + * [BC BREAK] fixed: form constraints are only validated if they belong to the validated group + * deprecated `bindRequest` in `Form` and replaced it by a listener to FormEvents::PRE_BIND + * fixed: the "data" option supersedes default values from the model + * changed DateType to refer to the "format" option for calculating the year and day choices instead + of padding them automatically + * [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now if the widget is + "single_text", in order to support the HTML 5 date field out of the box + * added the option "format" to DateTimeType + * [BC BREAK] DateTimeType now outputs RFC 3339 dates by default, as generated and + consumed by HTML5 browsers, if the widget is "single_text" + * deprecated the options "data_timezone" and "user_timezone" in DateType, DateTimeType and TimeType + and renamed them to "model_timezone" and "view_timezone" + * fixed: TransformationFailedExceptions thrown in the model transformer are now caught by the form + * added FormRegistryInterface, ResolvedFormTypeInterface and ResolvedFormTypeFactoryInterface + * deprecated FormFactory methods + * `addType` + * `hasType` + * `getType` + * [BC BREAK] FormFactory now expects a FormRegistryInterface and a ResolvedFormTypeFactoryInterface as constructor argument + * [BC BREAK] The method `createBuilder` in FormTypeInterface is not supported anymore for performance reasons + * [BC BREAK] Removed `setTypes` from FormBuilder + * deprecated AbstractType methods + * `getExtensions` + * `setExtensions` + * ChoiceType now caches its created choice lists to improve performance + * [BC BREAK] Rows of a collection field cannot be themed individually anymore. All rows in the collection + field now have the same block names, which contains "entry" where it previously contained the row index. + * [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name. + * added FormRendererInterface, FormRendererEngineInterface and implementations of these interfaces + * [BC BREAK] removed the following methods from FormUtil: + * `toArrayKey` + * `toArrayKeys` + * `isChoiceGroup` + * `isChoiceSelected` + * [BC BREAK] renamed method `renderBlock` in FormHelper to `block` and changed its signature + * made FormView properties public and deprecated their accessor methods + * made the normalized data of a form accessible in the template through the variable "form.vars.data" + * made the original data of a choice accessible in the template through the property "choice.data" + * added convenience class Forms and FormFactoryBuilderInterface diff --git a/lib/symfony/form/CallbackTransformer.php b/lib/symfony/form/CallbackTransformer.php new file mode 100644 index 0000000000..2a79b5b365 --- /dev/null +++ b/lib/symfony/form/CallbackTransformer.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\Form; + +class CallbackTransformer implements DataTransformerInterface +{ + private \Closure $transform; + private \Closure $reverseTransform; + + public function __construct(callable $transform, callable $reverseTransform) + { + $this->transform = $transform(...); + $this->reverseTransform = $reverseTransform(...); + } + + public function transform(mixed $data): mixed + { + return ($this->transform)($data); + } + + public function reverseTransform(mixed $data): mixed + { + return ($this->reverseTransform)($data); + } +} diff --git a/lib/symfony/form/ChoiceList/ArrayChoiceList.php b/lib/symfony/form/ChoiceList/ArrayChoiceList.php new file mode 100644 index 0000000000..8b5deb3f49 --- /dev/null +++ b/lib/symfony/form/ChoiceList/ArrayChoiceList.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +/** + * A list of choices with arbitrary data types. + * + * The user of this class is responsible for assigning string values to the + * choices and for their uniqueness. + * Both the choices and their values are passed to the constructor. + * Each choice must have a corresponding value (with the same key) in + * the values array. + * + * @author Bernhard Schussek + */ +class ArrayChoiceList implements ChoiceListInterface +{ + /** + * The choices in the list. + * + * @var array + */ + protected $choices; + + /** + * The values indexed by the original keys. + * + * @var array + */ + protected $structuredValues; + + /** + * The original keys of the choices array. + * + * @var int[]|string[] + */ + protected $originalKeys; + protected $valueCallback; + + /** + * Creates a list with the given choices and values. + * + * The given choice array must have the same array keys as the value array. + * + * @param iterable $choices The selectable choices + * @param callable|null $value The callable for creating the value + * for a choice. If `null` is passed, + * incrementing integers are used as + * values + */ + public function __construct(iterable $choices, ?callable $value = null) + { + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + if (null === $value && $this->castableToString($choices)) { + $value = static fn ($choice) => false === $choice ? '0' : (string) $choice; + } + + if (null !== $value) { + // If a deterministic value generator was passed, use it later + $this->valueCallback = $value(...); + } else { + // Otherwise generate incrementing integers as values + $value = static function () { + static $i = 0; + + return $i++; + }; + } + + // If the choices are given as recursive array (i.e. with explicit + // choice groups), flatten the array. The grouping information is needed + // in the view only. + $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues); + + $this->choices = $choicesByValues; + $this->originalKeys = $keysByValues; + $this->structuredValues = $structuredValues; + } + + public function getChoices(): array + { + return $this->choices; + } + + public function getValues(): array + { + return array_map('strval', array_keys($this->choices)); + } + + public function getStructuredValues(): array + { + return $this->structuredValues; + } + + public function getOriginalKeys(): array + { + return $this->originalKeys; + } + + public function getChoicesForValues(array $values): array + { + $choices = []; + + foreach ($values as $i => $givenValue) { + if (\array_key_exists($givenValue ?? '', $this->choices)) { + $choices[$i] = $this->choices[$givenValue]; + } + } + + return $choices; + } + + public function getValuesForChoices(array $choices): array + { + $values = []; + + // Use the value callback to compare choices by their values, if present + if ($this->valueCallback) { + $givenValues = []; + + foreach ($choices as $i => $givenChoice) { + $givenValues[$i] = (string) ($this->valueCallback)($givenChoice); + } + + return array_intersect($givenValues, array_keys($this->choices)); + } + + // Otherwise compare choices by identity + foreach ($choices as $i => $givenChoice) { + foreach ($this->choices as $value => $choice) { + if ($choice === $givenChoice) { + $values[$i] = (string) $value; + break; + } + } + } + + return $values; + } + + /** + * Flattens an array into the given output variables. + * + * @param array $choices The array to flatten + * @param callable $value The callable for generating choice values + * @param array|null $choicesByValues The flattened choices indexed by the + * corresponding values + * @param array|null $keysByValues The original keys indexed by the + * corresponding values + * @param array|null $structuredValues The values indexed by the original keys + * + * @internal + */ + protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues): void + { + if (null === $choicesByValues) { + $choicesByValues = []; + $keysByValues = []; + $structuredValues = []; + } + + foreach ($choices as $key => $choice) { + if (\is_array($choice)) { + $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]); + + continue; + } + + $choiceValue = (string) $value($choice); + $choicesByValues[$choiceValue] = $choice; + $keysByValues[$choiceValue] = $key; + $structuredValues[$key] = $choiceValue; + } + } + + /** + * Checks whether the given choices can be cast to strings without + * generating duplicates. + * This method is responsible for preventing conflict between scalar values + * and the empty value. + */ + private function castableToString(array $choices, array &$cache = []): bool + { + foreach ($choices as $choice) { + if (\is_array($choice)) { + if (!$this->castableToString($choice, $cache)) { + return false; + } + + continue; + } elseif (!\is_scalar($choice)) { + return false; + } + + // prevent having false casted to the empty string by isset() + $choice = false === $choice ? '0' : (string) $choice; + + if (isset($cache[$choice])) { + return false; + } + + $cache[$choice] = true; + } + + return true; + } +} diff --git a/lib/symfony/form/ChoiceList/ChoiceList.php b/lib/symfony/form/ChoiceList/ChoiceList.php new file mode 100644 index 0000000000..31166c1bdf --- /dev/null +++ b/lib/symfony/form/ChoiceList/ChoiceList.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue; +use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy; +use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice; +use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A set of convenient static methods to create cacheable choice list options. + * + * @author Jules Pietri + */ +final class ChoiceList +{ + /** + * Creates a cacheable loader from any callable providing iterable choices. + * + * @param callable $choices A callable that must return iterable choices or grouped choices + * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader + */ + public static function lazy(FormTypeInterface|FormTypeExtensionInterface $formType, callable $choices, mixed $vary = null): ChoiceLoader + { + return self::loader($formType, new CallbackChoiceLoader($choices), $vary); + } + + /** + * Decorates a loader to make it cacheable. + * + * @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices + * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader + */ + public static function loader(FormTypeInterface|FormTypeExtensionInterface $formType, ChoiceLoaderInterface $loader, mixed $vary = null): ChoiceLoader + { + return new ChoiceLoader($formType, $loader, $vary); + } + + /** + * Decorates a "choice_value" callback to make it cacheable. + * + * @param callable|array $value Any pseudo callable to create a unique string value from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback + */ + public static function value(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $value, mixed $vary = null): ChoiceValue + { + return new ChoiceValue($formType, $value, $vary); + } + + /** + * @param callable|array $filter Any pseudo callable to filter a choice list + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback + */ + public static function filter(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $filter, mixed $vary = null): ChoiceFilter + { + return new ChoiceFilter($formType, $filter, $vary); + } + + /** + * Decorates a "choice_label" option to make it cacheable. + * + * @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option + */ + public static function label(FormTypeInterface|FormTypeExtensionInterface $formType, callable|false $label, mixed $vary = null): ChoiceLabel + { + return new ChoiceLabel($formType, $label, $vary); + } + + /** + * Decorates a "choice_name" callback to make it cacheable. + * + * @param callable|array $fieldName Any pseudo callable to create a field name from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback + */ + public static function fieldName(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $fieldName, mixed $vary = null): ChoiceFieldName + { + return new ChoiceFieldName($formType, $fieldName, $vary); + } + + /** + * Decorates a "choice_attr" option to make it cacheable. + * + * @param callable|array $attr Any pseudo callable or array to create html attributes from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option + */ + public static function attr(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $attr, mixed $vary = null): ChoiceAttr + { + return new ChoiceAttr($formType, $attr, $vary); + } + + /** + * Decorates a "choice_translation_parameters" option to make it cacheable. + * + * @param callable|array $translationParameters Any pseudo callable or array to create translation parameters from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option + */ + public static function translationParameters(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $translationParameters, mixed $vary = null): ChoiceTranslationParameters + { + return new ChoiceTranslationParameters($formType, $translationParameters, $vary); + } + + /** + * Decorates a "group_by" callback to make it cacheable. + * + * @param callable|array $groupBy Any pseudo callable to return a group name from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback + */ + public static function groupBy(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $groupBy, mixed $vary = null): GroupBy + { + return new GroupBy($formType, $groupBy, $vary); + } + + /** + * Decorates a "preferred_choices" option to make it cacheable. + * + * @param callable|array $preferred Any pseudo callable or array to return a group name from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option + */ + public static function preferred(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $preferred, mixed $vary = null): PreferredChoice + { + return new PreferredChoice($formType, $preferred, $vary); + } + + /** + * Should not be instantiated. + */ + private function __construct() + { + } +} diff --git a/lib/symfony/form/ChoiceList/ChoiceListInterface.php b/lib/symfony/form/ChoiceList/ChoiceListInterface.php new file mode 100644 index 0000000000..e711a97edc --- /dev/null +++ b/lib/symfony/form/ChoiceList/ChoiceListInterface.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +/** + * A list of choices that can be selected in a choice field. + * + * A choice list assigns unique string values to each of a list of choices. + * These string values are displayed in the "value" attributes in HTML and + * submitted back to the server. + * + * The acceptable data types for the choices depend on the implementation. + * Values must always be strings and (within the list) free of duplicates. + * + * @author Bernhard Schussek + */ +interface ChoiceListInterface +{ + /** + * Returns all selectable choices. + * + * @return array The selectable choices indexed by the corresponding values + */ + public function getChoices(): array; + + /** + * Returns the values for the choices. + * + * The values are strings that do not contain duplicates: + * + * $form->add('field', 'choice', [ + * 'choices' => [ + * 'Decided' => ['Yes' => true, 'No' => false], + * 'Undecided' => ['Maybe' => null], + * ], + * ]); + * + * In this example, the result of this method is: + * + * [ + * 'Yes' => '0', + * 'No' => '1', + * 'Maybe' => '2', + * ] + * + * Null and false MUST NOT conflict when being casted to string. + * For this some default incremented values SHOULD be computed. + * + * @return string[] + */ + public function getValues(): array; + + /** + * Returns the values in the structure originally passed to the list. + * + * Contrary to {@link getValues()}, the result is indexed by the original + * keys of the choices. If the original array contained nested arrays, these + * nested arrays are represented here as well: + * + * $form->add('field', 'choice', [ + * 'choices' => [ + * 'Decided' => ['Yes' => true, 'No' => false], + * 'Undecided' => ['Maybe' => null], + * ], + * ]); + * + * In this example, the result of this method is: + * + * [ + * 'Decided' => ['Yes' => '0', 'No' => '1'], + * 'Undecided' => ['Maybe' => '2'], + * ] + * + * Nested arrays do not make sense in a view format unless + * they are used as a convenient way of grouping. + * If the implementation does not intend to support grouped choices, + * this method SHOULD be equivalent to {@link getValues()}. + * The $groupBy callback parameter SHOULD be used instead. + * + * @return string[] + */ + public function getStructuredValues(): array; + + /** + * Returns the original keys of the choices. + * + * The original keys are the keys of the choice array that was passed in the + * "choice" option of the choice type. Note that this array may contain + * duplicates if the "choice" option contained choice groups: + * + * $form->add('field', 'choice', [ + * 'choices' => [ + * 'Decided' => [true, false], + * 'Undecided' => [null], + * ], + * ]); + * + * In this example, the original key 0 appears twice, once for `true` and + * once for `null`. + * + * @return int[]|string[] The original choice keys indexed by the + * corresponding choice values + */ + public function getOriginalKeys(): array; + + /** + * Returns the choices corresponding to the given values. + * + * The choices are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * @param string[] $values An array of choice values. Non-existing values in + * this array are ignored + */ + public function getChoicesForValues(array $values): array; + + /** + * Returns the values corresponding to the given choices. + * + * The values are returned with the same keys and in the same order as the + * corresponding choices in the given array. + * + * @param array $choices An array of choices. Non-existing choices in this + * array are ignored + * + * @return string[] + */ + public function getValuesForChoices(array $choices): array; +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/AbstractStaticOption.php b/lib/symfony/form/ChoiceList/Factory/Cache/AbstractStaticOption.php new file mode 100644 index 0000000000..2686017c91 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/AbstractStaticOption.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A template decorator for static {@see ChoiceType} options. + * + * Used as fly weight for {@see CachingFactoryDecorator}. + * + * @internal + * + * @author Jules Pietri + */ +abstract class AbstractStaticOption +{ + private static array $options = []; + + private bool|string|array|\Closure|ChoiceLoaderInterface $option; + + /** + * @param mixed $option Any pseudo callable, array, string or bool to define a choice list option + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option + */ + final public function __construct(FormTypeInterface|FormTypeExtensionInterface $formType, mixed $option, mixed $vary = null) + { + $hash = CachingFactoryDecorator::generateHash([static::class, $formType, $vary]); + + $this->option = self::$options[$hash] ??= $option instanceof \Closure || \is_string($option) || \is_bool($option) || $option instanceof ChoiceLoaderInterface || !\is_callable($option) ? $option : $option(...); + } + + final public function getOption(): mixed + { + return $this->option; + } + + final public static function reset(): void + { + self::$options = []; + } +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceAttr.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceAttr.php new file mode 100644 index 0000000000..8de6956d16 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceAttr.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_attr" option. + * + * @internal + * + * @author Jules Pietri + */ +final class ChoiceAttr extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFieldName.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFieldName.php new file mode 100644 index 0000000000..0c71e20506 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFieldName.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_name" callback. + * + * @internal + * + * @author Jules Pietri + */ +final class ChoiceFieldName extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFilter.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFilter.php new file mode 100644 index 0000000000..13b8cd8ed3 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceFilter.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_filter" option. + * + * @internal + * + * @author Jules Pietri + */ +final class ChoiceFilter extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLabel.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLabel.php new file mode 100644 index 0000000000..664a09081f --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLabel.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_label" option. + * + * @internal + * + * @author Jules Pietri + */ +final class ChoiceLabel extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLoader.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLoader.php new file mode 100644 index 0000000000..1d64f101c0 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceLoader.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\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_loader" option. + * + * @internal + * + * @author Jules Pietri + */ +final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface +{ + public function loadChoiceList(?callable $value = null): ChoiceListInterface + { + return $this->getOption()->loadChoiceList($value); + } + + public function loadChoicesForValues(array $values, ?callable $value = null): array + { + return $this->getOption()->loadChoicesForValues($values, $value); + } + + public function loadValuesForChoices(array $choices, ?callable $value = null): array + { + return $this->getOption()->loadValuesForChoices($choices, $value); + } +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceTranslationParameters.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceTranslationParameters.php new file mode 100644 index 0000000000..e9ab5c7119 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceTranslationParameters.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_translation_parameters" option. + * + * @internal + * + * @author Vincent Langlet + */ +final class ChoiceTranslationParameters extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceValue.php b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceValue.php new file mode 100644 index 0000000000..d96f1e9e83 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/ChoiceValue.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "choice_value" callback. + * + * @internal + * + * @author Jules Pietri + */ +final class ChoiceValue extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/GroupBy.php b/lib/symfony/form/ChoiceList/Factory/Cache/GroupBy.php new file mode 100644 index 0000000000..2ad492caf3 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/GroupBy.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "group_by" callback. + * + * @internal + * + * @author Jules Pietri + */ +final class GroupBy extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/Cache/PreferredChoice.php b/lib/symfony/form/ChoiceList/Factory/Cache/PreferredChoice.php new file mode 100644 index 0000000000..4aefd69ab3 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/Cache/PreferredChoice.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory\Cache; + +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeInterface; + +/** + * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface} + * which configures a "preferred_choices" option. + * + * @internal + * + * @author Jules Pietri + */ +final class PreferredChoice extends AbstractStaticOption +{ +} diff --git a/lib/symfony/form/ChoiceList/Factory/CachingFactoryDecorator.php b/lib/symfony/form/ChoiceList/Factory/CachingFactoryDecorator.php new file mode 100644 index 0000000000..03bdff5dc9 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Caches the choice lists created by the decorated factory. + * + * To cache a list based on its options, arguments must be decorated + * by a {@see Cache\AbstractStaticOption} implementation. + * + * @author Bernhard Schussek + * @author Jules Pietri + */ +class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface +{ + private ChoiceListFactoryInterface $decoratedFactory; + + /** + * @var ChoiceListInterface[] + */ + private array $lists = []; + + /** + * @var ChoiceListView[] + */ + private array $views = []; + + /** + * Generates a SHA-256 hash for the given value. + * + * Optionally, a namespace string can be passed. Calling this method will + * the same values, but different namespaces, will return different hashes. + * + * @return string The SHA-256 hash + * + * @internal + */ + public static function generateHash(mixed $value, string $namespace = ''): string + { + if (\is_object($value)) { + $value = spl_object_hash($value); + } elseif (\is_array($value)) { + array_walk_recursive($value, static function (&$v) { + if (\is_object($v)) { + $v = spl_object_hash($v); + } + }); + } + + return hash('sha256', $namespace.':'.serialize($value)); + } + + public function __construct(ChoiceListFactoryInterface $decoratedFactory) + { + $this->decoratedFactory = $decoratedFactory; + } + + /** + * Returns the decorated factory. + */ + public function getDecoratedFactory(): ChoiceListFactoryInterface + { + return $this->decoratedFactory; + } + + public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface + { + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + $cache = true; + // Only cache per value and filter when needed. The value is not validated on purpose. + // The decorated factory may decide which values to accept and which not. + if ($value instanceof Cache\ChoiceValue) { + $value = $value->getOption(); + } elseif ($value) { + $cache = false; + } + if ($filter instanceof Cache\ChoiceFilter) { + $filter = $filter->getOption(); + } elseif ($filter) { + $cache = false; + } + + if (!$cache) { + return $this->decoratedFactory->createListFromChoices($choices, $value, $filter); + } + + $hash = self::generateHash([$choices, $value, $filter], 'fromChoices'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value, $filter); + } + + return $this->lists[$hash]; + } + + public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface + { + $cache = true; + + if ($loader instanceof Cache\ChoiceLoader) { + $loader = $loader->getOption(); + } else { + $cache = false; + } + + if ($value instanceof Cache\ChoiceValue) { + $value = $value->getOption(); + } elseif ($value) { + $cache = false; + } + + if ($filter instanceof Cache\ChoiceFilter) { + $filter = $filter->getOption(); + } elseif ($filter) { + $cache = false; + } + + if (!$cache) { + return $this->decoratedFactory->createListFromLoader($loader, $value, $filter); + } + + $hash = self::generateHash([$loader, $value, $filter], 'fromLoader'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value, $filter); + } + + return $this->lists[$hash]; + } + + /** + * @param bool $duplicatePreferredChoices + */ + public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView + { + $duplicatePreferredChoices = \func_num_args() > 7 ? func_get_arg(7) : true; + $cache = true; + + if ($preferredChoices instanceof Cache\PreferredChoice) { + $preferredChoices = $preferredChoices->getOption(); + } elseif ($preferredChoices) { + $cache = false; + } + + if ($label instanceof Cache\ChoiceLabel) { + $label = $label->getOption(); + } elseif (null !== $label) { + $cache = false; + } + + if ($index instanceof Cache\ChoiceFieldName) { + $index = $index->getOption(); + } elseif ($index) { + $cache = false; + } + + if ($groupBy instanceof Cache\GroupBy) { + $groupBy = $groupBy->getOption(); + } elseif ($groupBy) { + $cache = false; + } + + if ($attr instanceof Cache\ChoiceAttr) { + $attr = $attr->getOption(); + } elseif ($attr) { + $cache = false; + } + + if ($labelTranslationParameters instanceof Cache\ChoiceTranslationParameters) { + $labelTranslationParameters = $labelTranslationParameters->getOption(); + } elseif ([] !== $labelTranslationParameters) { + $cache = false; + } + + if (!$cache) { + return $this->decoratedFactory->createView( + $list, + $preferredChoices, + $label, + $index, + $groupBy, + $attr, + $labelTranslationParameters, + $duplicatePreferredChoices, + ); + } + + $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters, $duplicatePreferredChoices]); + + if (!isset($this->views[$hash])) { + $this->views[$hash] = $this->decoratedFactory->createView( + $list, + $preferredChoices, + $label, + $index, + $groupBy, + $attr, + $labelTranslationParameters, + $duplicatePreferredChoices, + ); + } + + return $this->views[$hash]; + } + + /** + * @return void + */ + public function reset() + { + $this->lists = []; + $this->views = []; + Cache\AbstractStaticOption::reset(); + } +} diff --git a/lib/symfony/form/ChoiceList/Factory/ChoiceListFactoryInterface.php b/lib/symfony/form/ChoiceList/Factory/ChoiceListFactoryInterface.php new file mode 100644 index 0000000000..7820af003a --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; + +/** + * Creates {@link ChoiceListInterface} instances. + * + * @author Bernhard Schussek + */ +interface ChoiceListFactoryInterface +{ + /** + * Creates a choice list for the given choices. + * + * The choices should be passed in the values of the choices array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as only argument. + * Null may be passed when the choice list contains the empty value. + * + * @param callable|null $filter The callable filtering the choices + */ + public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface; + + /** + * Creates a choice list that is loaded with the given loader. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as only argument. + * Null may be passed when the choice list contains the empty value. + * + * @param callable|null $filter The callable filtering the choices + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface; + + /** + * Creates a view for the given choice list. + * + * Callables may be passed for all optional arguments. The callables receive + * the choice as first and the array key as the second argument. + * + * * The callable for the label and the name should return the generated + * label/choice name. + * * The callable for the preferred choices should return true or false, + * depending on whether the choice should be preferred or not. + * * The callable for the grouping should return the group name or null if + * a choice should not be grouped. + * * The callable for the attributes should return an array of HTML + * attributes that will be inserted in the tag of the choice. + * + * If no callable is passed, the labels will be generated from the choice + * keys. The view indices will be generated using an incrementing integer + * by default. + * + * The preferred choices can also be passed as array. Each choice that is + * contained in that array will be marked as preferred. + * + * The attributes can be passed as multi-dimensional array. The keys should + * match the keys of the choices. The values should be arrays of HTML + * attributes that should be added to the respective choice. + * + * @param array|callable|null $preferredChoices The preferred choices + * @param callable|false|null $label The callable generating the choice labels; + * pass false to discard the label + * @param array|callable|null $attr The callable generating the HTML attributes + * @param array|callable $labelTranslationParameters The parameters used to translate the choice labels + * @param bool $duplicatePreferredChoices Whether the preferred choices should be duplicated + * on top of the list and in their original position + * or only in the top of the list + */ + public function createView(ChoiceListInterface $list, array|callable|null $preferredChoices = null, callable|false|null $label = null, ?callable $index = null, ?callable $groupBy = null, array|callable|null $attr = null, array|callable $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView; +} diff --git a/lib/symfony/form/ChoiceList/Factory/DefaultChoiceListFactory.php b/lib/symfony/form/ChoiceList/Factory/DefaultChoiceListFactory.php new file mode 100644 index 0000000000..849421f787 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -0,0 +1,307 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Contracts\Translation\TranslatableInterface; + +/** + * Default implementation of {@link ChoiceListFactoryInterface}. + * + * @author Bernhard Schussek + * @author Jules Pietri + */ +class DefaultChoiceListFactory implements ChoiceListFactoryInterface +{ + public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface + { + if ($filter) { + // filter the choice list lazily + return $this->createListFromLoader(new FilterChoiceLoaderDecorator( + new CallbackChoiceLoader(static fn () => $choices), + $filter + ), $value); + } + + return new ArrayChoiceList($choices, $value); + } + + public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface + { + if ($filter) { + $loader = new FilterChoiceLoaderDecorator($loader, $filter); + } + + return new LazyChoiceList($loader, $value); + } + + /** + * @param bool $duplicatePreferredChoices + */ + public function createView(ChoiceListInterface $list, array|callable|null $preferredChoices = null, callable|false|null $label = null, ?callable $index = null, ?callable $groupBy = null, array|callable|null $attr = null, array|callable $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView + { + $duplicatePreferredChoices = \func_num_args() > 7 ? func_get_arg(7) : true; + $preferredViews = []; + $preferredViewsOrder = []; + $otherViews = []; + $choices = $list->getChoices(); + $keys = $list->getOriginalKeys(); + + if (!\is_callable($preferredChoices)) { + if (!$preferredChoices) { + $preferredChoices = null; + } else { + // make sure we have keys that reflect order + $preferredChoices = array_values($preferredChoices); + $preferredChoices = static fn ($choice) => array_search($choice, $preferredChoices, true); + } + } + + // The names are generated from an incrementing integer by default + $index ??= 0; + + // If $groupBy is a callable returning a string + // choices are added to the group with the name returned by the callable. + // If $groupBy is a callable returning an array + // choices are added to the groups with names returned by the callable + // If the callable returns null, the choice is not added to any group + if (\is_callable($groupBy)) { + foreach ($choices as $value => $choice) { + self::addChoiceViewsGroupedByCallable( + $groupBy, + $choice, + $value, + $label, + $keys, + $index, + $attr, + $labelTranslationParameters, + $preferredChoices, + $preferredViews, + $preferredViewsOrder, + $otherViews, + $duplicatePreferredChoices, + ); + } + + // Remove empty group views that may have been created by + // addChoiceViewsGroupedByCallable() + foreach ($preferredViews as $key => $view) { + if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) { + unset($preferredViews[$key]); + } + } + + foreach ($otherViews as $key => $view) { + if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) { + unset($otherViews[$key]); + } + } + + foreach ($preferredViewsOrder as $key => $groupViewsOrder) { + if ($groupViewsOrder) { + $preferredViewsOrder[$key] = min($groupViewsOrder); + } else { + unset($preferredViewsOrder[$key]); + } + } + } else { + // Otherwise use the original structure of the choices + self::addChoiceViewsFromStructuredValues( + $list->getStructuredValues(), + $label, + $choices, + $keys, + $index, + $attr, + $labelTranslationParameters, + $preferredChoices, + $preferredViews, + $preferredViewsOrder, + $otherViews, + $duplicatePreferredChoices, + ); + } + + uksort($preferredViews, static fn ($a, $b) => isset($preferredViewsOrder[$a], $preferredViewsOrder[$b]) ? $preferredViewsOrder[$a] <=> $preferredViewsOrder[$b] : 0); + + return new ChoiceListView($otherViews, $preferredViews); + } + + private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void + { + // $value may be an integer or a string, since it's stored in the array + // keys. We want to guarantee it's a string though. + $key = $keys[$value]; + $nextIndex = \is_int($index) ? $index++ : $index($choice, $key, $value); + + // BC normalize label to accept a false value + if (null === $label) { + // If the labels are null, use the original choice key by default + $label = (string) $key; + } elseif (false !== $label) { + // If "choice_label" is set to false and "expanded" is true, the value false + // should be passed on to the "label" option of the checkboxes/radio buttons + $dynamicLabel = $label($choice, $key, $value); + + if (false === $dynamicLabel) { + $label = false; + } elseif ($dynamicLabel instanceof TranslatableInterface) { + $label = $dynamicLabel; + } else { + $label = (string) $dynamicLabel; + } + } + + $view = new ChoiceView( + $choice, + $value, + $label, + // The attributes may be a callable or a mapping from choice indices + // to nested arrays + \is_callable($attr) ? $attr($choice, $key, $value) : ($attr[$key] ?? []), + // The label translation parameters may be a callable or a mapping from choice indices + // to nested arrays + \is_callable($labelTranslationParameters) ? $labelTranslationParameters($choice, $key, $value) : ($labelTranslationParameters[$key] ?? []) + ); + + // $isPreferred may be null if no choices are preferred + if (null !== $isPreferred && false !== $preferredKey = $isPreferred($choice, $key, $value)) { + $preferredViews[$nextIndex] = $view; + $preferredViewsOrder[$nextIndex] = $preferredKey; + + if ($duplicatePreferredChoices) { + $otherViews[$nextIndex] = $view; + } + } else { + $otherViews[$nextIndex] = $view; + } + } + + private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void + { + foreach ($values as $key => $value) { + if (null === $value) { + continue; + } + + // Add the contents of groups to new ChoiceGroupView instances + if (\is_array($value)) { + $preferredViewsForGroup = []; + $otherViewsForGroup = []; + + self::addChoiceViewsFromStructuredValues( + $value, + $label, + $choices, + $keys, + $index, + $attr, + $labelTranslationParameters, + $isPreferred, + $preferredViewsForGroup, + $preferredViewsOrder, + $otherViewsForGroup, + $duplicatePreferredChoices, + ); + + if (\count($preferredViewsForGroup) > 0) { + $preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup); + } + + if (\count($otherViewsForGroup) > 0) { + $otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup); + } + + continue; + } + + // Add ungrouped items directly + self::addChoiceView( + $choices[$value], + $value, + $label, + $keys, + $index, + $attr, + $labelTranslationParameters, + $isPreferred, + $preferredViews, + $preferredViewsOrder, + $otherViews, + $duplicatePreferredChoices, + ); + } + } + + private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void + { + $groupLabels = $groupBy($choice, $keys[$value], $value); + + if (null === $groupLabels) { + // If the callable returns null, don't group the choice + self::addChoiceView( + $choice, + $value, + $label, + $keys, + $index, + $attr, + $labelTranslationParameters, + $isPreferred, + $preferredViews, + $preferredViewsOrder, + $otherViews, + $duplicatePreferredChoices, + ); + + return; + } + + $groupLabels = \is_array($groupLabels) ? array_map('strval', $groupLabels) : [(string) $groupLabels]; + + foreach ($groupLabels as $groupLabel) { + // Initialize the group views if necessary. Unnecessarily built group + // views will be cleaned up at the end of createView() + if (!isset($preferredViews[$groupLabel])) { + $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel); + $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel); + } + if (!isset($preferredViewsOrder[$groupLabel])) { + $preferredViewsOrder[$groupLabel] = []; + } + + self::addChoiceView( + $choice, + $value, + $label, + $keys, + $index, + $attr, + $labelTranslationParameters, + $isPreferred, + $preferredViews[$groupLabel]->choices, + $preferredViewsOrder[$groupLabel], + $otherViews[$groupLabel]->choices, + $duplicatePreferredChoices, + ); + } + } +} diff --git a/lib/symfony/form/ChoiceList/Factory/PropertyAccessDecorator.php b/lib/symfony/form/ChoiceList/Factory/PropertyAccessDecorator.php new file mode 100644 index 0000000000..e27c60420a --- /dev/null +++ b/lib/symfony/form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * Adds property path support to a choice list factory. + * + * Pass the decorated factory to the constructor: + * + * $decorator = new PropertyAccessDecorator($factory); + * + * You can now pass property paths for generating choice values, labels, view + * indices, HTML attributes and for determining the preferred choices and the + * choice groups: + * + * // extract values from the $value property + * $list = $createListFromChoices($objects, 'value'); + * + * @author Bernhard Schussek + */ +class PropertyAccessDecorator implements ChoiceListFactoryInterface +{ + private ChoiceListFactoryInterface $decoratedFactory; + private PropertyAccessorInterface $propertyAccessor; + + public function __construct(ChoiceListFactoryInterface $decoratedFactory, ?PropertyAccessorInterface $propertyAccessor = null) + { + $this->decoratedFactory = $decoratedFactory; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + /** + * Returns the decorated factory. + */ + public function getDecoratedFactory(): ChoiceListFactoryInterface + { + return $this->decoratedFactory; + } + + public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface + { + if (\is_string($value)) { + $value = new PropertyPath($value); + } + + if ($value instanceof PropertyPathInterface) { + $accessor = $this->propertyAccessor; + // The callable may be invoked with a non-object/array value + // when such values are passed to + // ChoiceListInterface::getValuesForChoices(). Handle this case + // so that the call to getValue() doesn't break. + $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null; + } + + if (\is_string($filter)) { + $filter = new PropertyPath($filter); + } + + if ($filter instanceof PropertyPath) { + $accessor = $this->propertyAccessor; + $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter); + } + + return $this->decoratedFactory->createListFromChoices($choices, $value, $filter); + } + + public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface + { + if (\is_string($value)) { + $value = new PropertyPath($value); + } + + if ($value instanceof PropertyPathInterface) { + $accessor = $this->propertyAccessor; + // The callable may be invoked with a non-object/array value + // when such values are passed to + // ChoiceListInterface::getValuesForChoices(). Handle this case + // so that the call to getValue() doesn't break. + $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null; + } + + if (\is_string($filter)) { + $filter = new PropertyPath($filter); + } + + if ($filter instanceof PropertyPath) { + $accessor = $this->propertyAccessor; + $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter); + } + + return $this->decoratedFactory->createListFromLoader($loader, $value, $filter); + } + + /** + * @param bool $duplicatePreferredChoices + */ + public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView + { + $duplicatePreferredChoices = \func_num_args() > 7 ? func_get_arg(7) : true; + $accessor = $this->propertyAccessor; + + if (\is_string($label)) { + $label = new PropertyPath($label); + } + + if ($label instanceof PropertyPathInterface) { + $label = static fn ($choice) => $accessor->getValue($choice, $label); + } + + if (\is_string($preferredChoices)) { + $preferredChoices = new PropertyPath($preferredChoices); + } + + if ($preferredChoices instanceof PropertyPathInterface) { + $preferredChoices = static function ($choice) use ($accessor, $preferredChoices) { + try { + return $accessor->getValue($choice, $preferredChoices); + } catch (UnexpectedTypeException) { + // Assume not preferred if not readable + return false; + } + }; + } + + if (\is_string($index)) { + $index = new PropertyPath($index); + } + + if ($index instanceof PropertyPathInterface) { + $index = static fn ($choice) => $accessor->getValue($choice, $index); + } + + if (\is_string($groupBy)) { + $groupBy = new PropertyPath($groupBy); + } + + if ($groupBy instanceof PropertyPathInterface) { + $groupBy = static function ($choice) use ($accessor, $groupBy) { + try { + return $accessor->getValue($choice, $groupBy); + } catch (UnexpectedTypeException) { + // Don't group if path is not readable + return null; + } + }; + } + + if (\is_string($attr)) { + $attr = new PropertyPath($attr); + } + + if ($attr instanceof PropertyPathInterface) { + $attr = static fn ($choice) => $accessor->getValue($choice, $attr); + } + + if (\is_string($labelTranslationParameters)) { + $labelTranslationParameters = new PropertyPath($labelTranslationParameters); + } + + if ($labelTranslationParameters instanceof PropertyPath) { + $labelTranslationParameters = static fn ($choice) => $accessor->getValue($choice, $labelTranslationParameters); + } + + return $this->decoratedFactory->createView( + $list, + $preferredChoices, + $label, + $index, + $groupBy, + $attr, + $labelTranslationParameters, + $duplicatePreferredChoices, + ); + } +} diff --git a/lib/symfony/form/ChoiceList/LazyChoiceList.php b/lib/symfony/form/ChoiceList/LazyChoiceList.php new file mode 100644 index 0000000000..2f79189260 --- /dev/null +++ b/lib/symfony/form/ChoiceList/LazyChoiceList.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + +/** + * A choice list that loads its choices lazily. + * + * The choices are fetched using a {@link ChoiceLoaderInterface} instance. + * If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is + * called, the choice list is only loaded partially for improved performance. + * + * Once {@link getChoices()} or {@link getValues()} is called, the list is + * loaded fully. + * + * @author Bernhard Schussek + */ +class LazyChoiceList implements ChoiceListInterface +{ + private ChoiceLoaderInterface $loader; + + /** + * The callable creating string values for each choice. + * + * If null, choices are cast to strings. + */ + private ?\Closure $value; + + /** + * Creates a lazily-loaded list using the given loader. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param callable|null $value The callable generating the choice values + */ + public function __construct(ChoiceLoaderInterface $loader, ?callable $value = null) + { + $this->loader = $loader; + $this->value = null === $value ? null : $value(...); + } + + public function getChoices(): array + { + return $this->loader->loadChoiceList($this->value)->getChoices(); + } + + public function getValues(): array + { + return $this->loader->loadChoiceList($this->value)->getValues(); + } + + public function getStructuredValues(): array + { + return $this->loader->loadChoiceList($this->value)->getStructuredValues(); + } + + public function getOriginalKeys(): array + { + return $this->loader->loadChoiceList($this->value)->getOriginalKeys(); + } + + public function getChoicesForValues(array $values): array + { + return $this->loader->loadChoicesForValues($values, $this->value); + } + + public function getValuesForChoices(array $choices): array + { + return $this->loader->loadValuesForChoices($choices, $this->value); + } +} diff --git a/lib/symfony/form/ChoiceList/Loader/AbstractChoiceLoader.php b/lib/symfony/form/ChoiceList/Loader/AbstractChoiceLoader.php new file mode 100644 index 0000000000..749e2fbcef --- /dev/null +++ b/lib/symfony/form/ChoiceList/Loader/AbstractChoiceLoader.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + +/** + * @author Jules Pietri + */ +abstract class AbstractChoiceLoader implements ChoiceLoaderInterface +{ + private ?iterable $choices; + + /** + * @final + */ + public function loadChoiceList(?callable $value = null): ChoiceListInterface + { + return new ArrayChoiceList($this->choices ??= $this->loadChoices(), $value); + } + + public function loadChoicesForValues(array $values, ?callable $value = null): array + { + if (!$values) { + return []; + } + + return $this->doLoadChoicesForValues($values, $value); + } + + public function loadValuesForChoices(array $choices, ?callable $value = null): array + { + if (!$choices) { + return []; + } + + if ($value) { + // if a value callback exists, use it + return array_map(fn ($item) => (string) $value($item), $choices); + } + + return $this->doLoadValuesForChoices($choices); + } + + abstract protected function loadChoices(): iterable; + + protected function doLoadChoicesForValues(array $values, ?callable $value): array + { + return $this->loadChoiceList($value)->getChoicesForValues($values); + } + + protected function doLoadValuesForChoices(array $choices): array + { + return $this->loadChoiceList()->getValuesForChoices($choices); + } +} diff --git a/lib/symfony/form/ChoiceList/Loader/CallbackChoiceLoader.php b/lib/symfony/form/ChoiceList/Loader/CallbackChoiceLoader.php new file mode 100644 index 0000000000..088f91dae2 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Loader/CallbackChoiceLoader.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +/** + * Loads an {@link ArrayChoiceList} instance from a callable returning iterable choices. + * + * @author Jules Pietri + */ +class CallbackChoiceLoader extends AbstractChoiceLoader +{ + private \Closure $callback; + + /** + * @param callable $callback The callable returning iterable choices + */ + public function __construct(callable $callback) + { + $this->callback = $callback(...); + } + + protected function loadChoices(): iterable + { + return ($this->callback)(); + } +} diff --git a/lib/symfony/form/ChoiceList/Loader/ChoiceLoaderInterface.php b/lib/symfony/form/ChoiceList/Loader/ChoiceLoaderInterface.php new file mode 100644 index 0000000000..d5f803c778 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Loader/ChoiceLoaderInterface.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + +/** + * Loads a choice list. + * + * The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()} + * can be used to load the list only partially in cases where a fully-loaded + * list is not necessary. + * + * @author Bernhard Schussek + */ +interface ChoiceLoaderInterface +{ + /** + * Loads a list of choices. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as only argument. + * Null may be passed when the choice list contains the empty value. + * + * @param callable|null $value The callable which generates the values + * from choices + */ + public function loadChoiceList(?callable $value = null): ChoiceListInterface; + + /** + * Loads the choices corresponding to the given values. + * + * The choices are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as only argument. + * Null may be passed when the choice list contains the empty value. + * + * @param string[] $values An array of choice values. Non-existing + * values in this array are ignored + * @param callable|null $value The callable generating the choice values + */ + public function loadChoicesForValues(array $values, ?callable $value = null): array; + + /** + * Loads the values corresponding to the given choices. + * + * The values are returned with the same keys and in the same order as the + * corresponding choices in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as only argument. + * Null may be passed when the choice list contains the empty value. + * + * @param array $choices An array of choices. Non-existing choices in + * this array are ignored + * @param callable|null $value The callable generating the choice values + * + * @return string[] + */ + public function loadValuesForChoices(array $choices, ?callable $value = null): array; +} diff --git a/lib/symfony/form/ChoiceList/Loader/FilterChoiceLoaderDecorator.php b/lib/symfony/form/ChoiceList/Loader/FilterChoiceLoaderDecorator.php new file mode 100644 index 0000000000..393c73eba8 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Loader/FilterChoiceLoaderDecorator.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +/** + * A decorator to filter choices only when they are loaded or partially loaded. + * + * @author Jules Pietri + */ +class FilterChoiceLoaderDecorator extends AbstractChoiceLoader +{ + private ChoiceLoaderInterface $decoratedLoader; + private \Closure $filter; + + public function __construct(ChoiceLoaderInterface $loader, callable $filter) + { + $this->decoratedLoader = $loader; + $this->filter = $filter(...); + } + + protected function loadChoices(): iterable + { + $list = $this->decoratedLoader->loadChoiceList(); + + if (array_values($list->getValues()) === array_values($structuredValues = $list->getStructuredValues())) { + return array_filter(array_combine($list->getOriginalKeys(), $list->getChoices()), $this->filter); + } + + foreach ($structuredValues as $group => $values) { + if (\is_array($values)) { + if ($values && $filtered = array_filter($list->getChoicesForValues($values), $this->filter)) { + $choices[$group] = $filtered; + } + continue; + // filter empty groups + } + + if ($filtered = array_filter($list->getChoicesForValues([$values]), $this->filter)) { + $choices[$group] = $filtered[0]; + } + } + + return $choices ?? []; + } + + public function loadChoicesForValues(array $values, ?callable $value = null): array + { + return array_filter($this->decoratedLoader->loadChoicesForValues($values, $value), $this->filter); + } + + public function loadValuesForChoices(array $choices, ?callable $value = null): array + { + return $this->decoratedLoader->loadValuesForChoices(array_filter($choices, $this->filter), $value); + } +} diff --git a/lib/symfony/form/ChoiceList/Loader/IntlCallbackChoiceLoader.php b/lib/symfony/form/ChoiceList/Loader/IntlCallbackChoiceLoader.php new file mode 100644 index 0000000000..0931d3ef56 --- /dev/null +++ b/lib/symfony/form/ChoiceList/Loader/IntlCallbackChoiceLoader.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +/** + * Callback choice loader optimized for Intl choice types. + * + * @author Jules Pietri + * @author Yonel Ceruto + */ +class IntlCallbackChoiceLoader extends CallbackChoiceLoader +{ + public function loadChoicesForValues(array $values, ?callable $value = null): array + { + return parent::loadChoicesForValues(array_filter($values), $value); + } + + public function loadValuesForChoices(array $choices, ?callable $value = null): array + { + $choices = array_filter($choices); + + // If no callable is set, choices are the same as values + if (null === $value) { + return $choices; + } + + return parent::loadValuesForChoices($choices, $value); + } +} diff --git a/lib/symfony/form/ChoiceList/View/ChoiceGroupView.php b/lib/symfony/form/ChoiceList/View/ChoiceGroupView.php new file mode 100644 index 0000000000..64fe3baec3 --- /dev/null +++ b/lib/symfony/form/ChoiceList/View/ChoiceGroupView.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a group of choices in templates. + * + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class ChoiceGroupView implements \IteratorAggregate +{ + public $label; + public $choices; + + /** + * Creates a new choice group view. + * + * @param array $choices the choice views in the group + */ + public function __construct(string $label, array $choices = []) + { + $this->label = $label; + $this->choices = $choices; + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->choices); + } +} diff --git a/lib/symfony/form/ChoiceList/View/ChoiceListView.php b/lib/symfony/form/ChoiceList/View/ChoiceListView.php new file mode 100644 index 0000000000..949174e3a7 --- /dev/null +++ b/lib/symfony/form/ChoiceList/View/ChoiceListView.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\Form\ChoiceList\View; + +/** + * Represents a choice list in templates. + * + * A choice list contains choices and optionally preferred choices which are + * displayed in the very beginning of the list. Both choices and preferred + * choices may be grouped in {@link ChoiceGroupView} instances. + * + * @author Bernhard Schussek + */ +class ChoiceListView +{ + public $choices; + public $preferredChoices; + + /** + * Creates a new choice list view. + * + * @param array $choices The choice views + * @param array $preferredChoices the preferred choice views + */ + public function __construct(array $choices = [], array $preferredChoices = []) + { + $this->choices = $choices; + $this->preferredChoices = $preferredChoices; + } + + /** + * Returns whether a placeholder is in the choices. + * + * A placeholder must be the first child element, not be in a group and have an empty value. + */ + public function hasPlaceholder(): bool + { + if ($this->preferredChoices) { + $firstChoice = reset($this->preferredChoices); + + return $firstChoice instanceof ChoiceView && '' === $firstChoice->value; + } + + $firstChoice = reset($this->choices); + + return $firstChoice instanceof ChoiceView && '' === $firstChoice->value; + } +} diff --git a/lib/symfony/form/ChoiceList/View/ChoiceView.php b/lib/symfony/form/ChoiceList/View/ChoiceView.php new file mode 100644 index 0000000000..050d8ed243 --- /dev/null +++ b/lib/symfony/form/ChoiceList/View/ChoiceView.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\Form\ChoiceList\View; + +use Symfony\Contracts\Translation\TranslatableInterface; + +/** + * Represents a choice in templates. + * + * @author Bernhard Schussek + */ +class ChoiceView +{ + public $label; + public $value; + public $data; + + /** + * Additional attributes for the HTML tag. + */ + public $attr; + + /** + * Additional parameters used to translate the label. + */ + public $labelTranslationParameters; + + /** + * Creates a new choice view. + * + * @param mixed $data The original choice + * @param string $value The view representation of the choice + * @param string|TranslatableInterface|false $label The label displayed to humans; pass false to discard the label + * @param array $attr Additional attributes for the HTML tag + * @param array $labelTranslationParameters Additional parameters used to translate the label + */ + public function __construct(mixed $data, string $value, string|TranslatableInterface|false $label, array $attr = [], array $labelTranslationParameters = []) + { + $this->data = $data; + $this->value = $value; + $this->label = $label; + $this->attr = $attr; + $this->labelTranslationParameters = $labelTranslationParameters; + } +} diff --git a/lib/symfony/form/ClearableErrorsInterface.php b/lib/symfony/form/ClearableErrorsInterface.php new file mode 100644 index 0000000000..a05ece05a8 --- /dev/null +++ b/lib/symfony/form/ClearableErrorsInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * A form element whose errors can be cleared. + * + * @author Colin O'Dell + */ +interface ClearableErrorsInterface +{ + /** + * Removes all the errors of this form. + * + * @param bool $deep Whether to remove errors from child forms as well + * + * @return $this + */ + public function clearErrors(bool $deep = false): static; +} diff --git a/lib/symfony/form/ClickableInterface.php b/lib/symfony/form/ClickableInterface.php new file mode 100644 index 0000000000..9be7de0ce8 --- /dev/null +++ b/lib/symfony/form/ClickableInterface.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\Form; + +/** + * A clickable form element. + * + * @author Bernhard Schussek + */ +interface ClickableInterface +{ + /** + * Returns whether this element was clicked. + */ + public function isClicked(): bool; +} diff --git a/lib/symfony/form/Command/DebugCommand.php b/lib/symfony/form/Command/DebugCommand.php new file mode 100644 index 0000000000..4ffb85fd95 --- /dev/null +++ b/lib/symfony/form/Command/DebugCommand.php @@ -0,0 +1,291 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +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\ErrorHandler\ErrorRenderer\FileLinkFormatter; +use Symfony\Component\Form\Console\Helper\DescriptorHelper; +use Symfony\Component\Form\Extension\Core\CoreExtension; +use Symfony\Component\Form\FormRegistryInterface; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter as LegacyFileLinkFormatter; + +/** + * A console command for retrieving information about form types. + * + * @author Yonel Ceruto + */ +#[AsCommand(name: 'debug:form', description: 'Display form type information')] +class DebugCommand extends Command +{ + private FormRegistryInterface $formRegistry; + private array $namespaces; + private array $types; + private array $extensions; + private array $guessers; + private FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter; + + public function __construct(FormRegistryInterface $formRegistry, array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'], array $types = [], array $extensions = [], array $guessers = [], FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter = null) + { + parent::__construct(); + + $this->formRegistry = $formRegistry; + $this->namespaces = $namespaces; + $this->types = $types; + $this->extensions = $extensions; + $this->guessers = $guessers; + $this->fileLinkFormatter = $fileLinkFormatter; + } + + /** + * @return void + */ + protected function configure() + { + $this + ->setDefinition([ + new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'), + new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'), + new InputOption('show-deprecated', null, InputOption::VALUE_NONE, 'Display deprecated options in form types'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'), + ]) + ->setHelp(<<<'EOF' +The %command.name% command displays information about form types. + + php %command.full_name% + +The command lists all built-in types, services types, type extensions and +guessers currently available. + + php %command.full_name% Symfony\Component\Form\Extension\Core\Type\ChoiceType + php %command.full_name% ChoiceType + +The command lists all defined options that contains the given form type, +as well as their parents and type extensions. + + php %command.full_name% ChoiceType choice_value + +Use the --show-deprecated option to display form types with +deprecated options or the deprecated options of the given form type: + + php %command.full_name% --show-deprecated + php %command.full_name% ChoiceType --show-deprecated + +The command displays the definition of the given option name. + + php %command.full_name% --format=json + +The command lists everything in a machine readable json format. +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + if (null === $class = $input->getArgument('class')) { + $object = null; + $options['core_types'] = $this->getCoreTypes(); + $options['service_types'] = array_values(array_diff($this->types, $options['core_types'])); + if ($input->getOption('show-deprecated')) { + $options['core_types'] = $this->filterTypesByDeprecated($options['core_types']); + $options['service_types'] = $this->filterTypesByDeprecated($options['service_types']); + } + $options['extensions'] = $this->extensions; + $options['guessers'] = $this->guessers; + foreach ($options as $k => $list) { + sort($options[$k]); + } + } else { + if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) { + $class = $this->getFqcnTypeClass($input, $io, $class); + } + $resolvedType = $this->formRegistry->getType($class); + + if ($option = $input->getArgument('option')) { + $object = $resolvedType->getOptionsResolver(); + + if (!$object->isDefined($option)) { + $message = \sprintf('Option "%s" is not defined in "%s".', $option, $resolvedType->getInnerType()::class); + + if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) { + if (1 === \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new InvalidArgumentException($message); + } + + $options['type'] = $resolvedType->getInnerType(); + $options['option'] = $option; + } else { + $object = $resolvedType; + } + } + + $helper = new DescriptorHelper($this->fileLinkFormatter); + $options['format'] = $input->getOption('format'); + $options['show_deprecated'] = $input->getOption('show-deprecated'); + $helper->describe($io, $object, $options); + + return 0; + } + + private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, string $shortClassName): string + { + $classes = $this->getFqcnTypeClasses($shortClassName); + + if (0 === $count = \count($classes)) { + $message = \sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)); + + $allTypes = array_merge($this->getCoreTypes(), $this->types); + if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) { + if (1 === \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new InvalidArgumentException($message); + } + if (1 === $count) { + return $classes[0]; + } + if (!$input->isInteractive()) { + throw new InvalidArgumentException(\sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s.", $shortClassName, implode("\n ", $classes))); + } + + return $io->choice(\sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]); + } + + private function getFqcnTypeClasses(string $shortClassName): array + { + $classes = []; + sort($this->namespaces); + foreach ($this->namespaces as $namespace) { + if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) { + $classes[] = $fqcn; + } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName))) { + $classes[] = $fqcn; + } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName).'Type')) { + $classes[] = $fqcn; + } elseif (str_ends_with($shortClassName, 'type') && class_exists($fqcn = $namespace.'\\'.ucfirst(substr($shortClassName, 0, -4).'Type'))) { + $classes[] = $fqcn; + } + } + + return $classes; + } + + private function getCoreTypes(): array + { + $coreExtension = new CoreExtension(); + $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes'); + $coreTypes = $loadTypesRefMethod->invoke($coreExtension); + $coreTypes = array_map(static fn (FormTypeInterface $type) => $type::class, $coreTypes); + sort($coreTypes); + + return $coreTypes; + } + + private function filterTypesByDeprecated(array $types): array + { + $typesWithDeprecatedOptions = []; + foreach ($types as $class) { + $optionsResolver = $this->formRegistry->getType($class)->getOptionsResolver(); + foreach ($optionsResolver->getDefinedOptions() as $option) { + if ($optionsResolver->isDeprecated($option)) { + $typesWithDeprecatedOptions[] = $class; + break; + } + } + } + + return $typesWithDeprecatedOptions; + } + + private function findAlternatives(string $name, array $collection): array + { + $alternatives = []; + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $threshold = 1e3; + $alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold); + ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); + + return array_keys($alternatives); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('class')) { + $suggestions->suggestValues(array_merge($this->getCoreTypes(), $this->types)); + + return; + } + + if ($input->mustSuggestArgumentValuesFor('option') && null !== $class = $input->getArgument('class')) { + $this->completeOptions($class, $suggestions); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues($this->getAvailableFormatOptions()); + } + } + + private function completeOptions(string $class, CompletionSuggestions $suggestions): void + { + if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) { + $classes = $this->getFqcnTypeClasses($class); + + if (1 === \count($classes)) { + $class = $classes[0]; + } + } + + if (!$this->formRegistry->hasType($class)) { + return; + } + + $resolvedType = $this->formRegistry->getType($class); + $suggestions->suggestValues($resolvedType->getOptionsResolver()->getDefinedOptions()); + } + + private function getAvailableFormatOptions(): array + { + return (new DescriptorHelper())->getFormats(); + } +} diff --git a/lib/symfony/form/Console/Descriptor/Descriptor.php b/lib/symfony/form/Console/Descriptor/Descriptor.php new file mode 100644 index 0000000000..64df41a179 --- /dev/null +++ b/lib/symfony/form/Console/Descriptor/Descriptor.php @@ -0,0 +1,195 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Console\Descriptor; + +use Symfony\Component\Console\Descriptor\DescriptorInterface; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\OutputStyle; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\Form\Util\OptionsResolverWrapper; +use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; +use Symfony\Component\OptionsResolver\Exception\NoConfigurationException; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Yonel Ceruto + * + * @internal + */ +abstract class Descriptor implements DescriptorInterface +{ + protected OutputStyle $output; + protected array $ownOptions = []; + protected array $overriddenOptions = []; + protected array $parentOptions = []; + protected array $extensionOptions = []; + protected array $requiredOptions = []; + protected array $parents = []; + protected array $extensions = []; + + public function describe(OutputInterface $output, ?object $object, array $options = []): void + { + $this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output); + + match (true) { + null === $object => $this->describeDefaults($options), + $object instanceof ResolvedFormTypeInterface => $this->describeResolvedFormType($object, $options), + $object instanceof OptionsResolver => $this->describeOption($object, $options), + default => throw new \InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))), + }; + } + + abstract protected function describeDefaults(array $options): void; + + abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void; + + abstract protected function describeOption(OptionsResolver $optionsResolver, array $options): void; + + protected function collectOptions(ResolvedFormTypeInterface $type): void + { + $this->parents = []; + $this->extensions = []; + + if (null !== $type->getParent()) { + $optionsResolver = clone $this->getParentOptionsResolver($type->getParent()); + } else { + $optionsResolver = new OptionsResolver(); + } + + $type->getInnerType()->configureOptions($ownOptionsResolver = new OptionsResolverWrapper()); + $this->ownOptions = array_diff($ownOptionsResolver->getDefinedOptions(), $optionsResolver->getDefinedOptions()); + $overriddenOptions = array_intersect(array_merge($ownOptionsResolver->getDefinedOptions(), $ownOptionsResolver->getUndefinedOptions()), $optionsResolver->getDefinedOptions()); + + $this->parentOptions = []; + foreach ($this->parents as $class => $parentOptions) { + $this->overriddenOptions[$class] = array_intersect($overriddenOptions, $parentOptions); + $this->parentOptions[$class] = array_diff($parentOptions, $overriddenOptions); + } + + $type->getInnerType()->configureOptions($optionsResolver); + $this->collectTypeExtensionsOptions($type, $optionsResolver); + $this->extensionOptions = []; + foreach ($this->extensions as $class => $extensionOptions) { + $this->overriddenOptions[$class] = array_intersect($overriddenOptions, $extensionOptions); + $this->extensionOptions[$class] = array_diff($extensionOptions, $overriddenOptions); + } + + $this->overriddenOptions = array_filter($this->overriddenOptions); + $this->parentOptions = array_filter($this->parentOptions); + $this->extensionOptions = array_filter($this->extensionOptions); + $this->requiredOptions = $optionsResolver->getRequiredOptions(); + + $this->parents = array_keys($this->parents); + $this->extensions = array_keys($this->extensions); + } + + protected function getOptionDefinition(OptionsResolver $optionsResolver, string $option): array + { + $definition = []; + + if ($info = $optionsResolver->getInfo($option)) { + $definition = [ + 'info' => $info, + ]; + } + + $definition += [ + 'required' => $optionsResolver->isRequired($option), + 'deprecated' => $optionsResolver->isDeprecated($option), + ]; + + $introspector = new OptionsResolverIntrospector($optionsResolver); + + $map = [ + 'default' => 'getDefault', + 'lazy' => 'getLazyClosures', + 'allowedTypes' => 'getAllowedTypes', + 'allowedValues' => 'getAllowedValues', + 'normalizers' => 'getNormalizers', + 'deprecation' => 'getDeprecation', + ]; + + foreach ($map as $key => $method) { + try { + $definition[$key] = $introspector->{$method}($option); + } catch (NoConfigurationException) { + // noop + } + } + + if (isset($definition['deprecation']) && isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) { + $definition['deprecationMessage'] = strtr($definition['deprecation']['message'], ['%name%' => $option]); + $definition['deprecationPackage'] = $definition['deprecation']['package']; + $definition['deprecationVersion'] = $definition['deprecation']['version']; + } + + return $definition; + } + + protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type): void + { + $deprecatedOptions = []; + $resolver = $type->getOptionsResolver(); + foreach ($resolver->getDefinedOptions() as $option) { + if ($resolver->isDeprecated($option)) { + $deprecatedOptions[] = $option; + } + } + + $filterByDeprecated = static function (array $options) use ($deprecatedOptions) { + foreach ($options as $class => $opts) { + if ($deprecated = array_intersect($deprecatedOptions, $opts)) { + $options[$class] = $deprecated; + } else { + unset($options[$class]); + } + } + + return $options; + }; + + $this->ownOptions = array_intersect($deprecatedOptions, $this->ownOptions); + $this->overriddenOptions = $filterByDeprecated($this->overriddenOptions); + $this->parentOptions = $filterByDeprecated($this->parentOptions); + $this->extensionOptions = $filterByDeprecated($this->extensionOptions); + } + + private function getParentOptionsResolver(ResolvedFormTypeInterface $type): OptionsResolver + { + $this->parents[$class = $type->getInnerType()::class] = []; + + if (null !== $type->getParent()) { + $optionsResolver = clone $this->getParentOptionsResolver($type->getParent()); + } else { + $optionsResolver = new OptionsResolver(); + } + + $inheritedOptions = $optionsResolver->getDefinedOptions(); + $type->getInnerType()->configureOptions($optionsResolver); + $this->parents[$class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions); + + $this->collectTypeExtensionsOptions($type, $optionsResolver); + + return $optionsResolver; + } + + private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver): void + { + foreach ($type->getTypeExtensions() as $extension) { + $inheritedOptions = $optionsResolver->getDefinedOptions(); + $extension->configureOptions($optionsResolver); + $this->extensions[$extension::class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions); + } + } +} diff --git a/lib/symfony/form/Console/Descriptor/JsonDescriptor.php b/lib/symfony/form/Console/Descriptor/JsonDescriptor.php new file mode 100644 index 0000000000..1f5c7bfa55 --- /dev/null +++ b/lib/symfony/form/Console/Descriptor/JsonDescriptor.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Console\Descriptor; + +use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Yonel Ceruto + * + * @internal + */ +class JsonDescriptor extends Descriptor +{ + protected function describeDefaults(array $options): void + { + $data['builtin_form_types'] = $options['core_types']; + $data['service_form_types'] = $options['service_types']; + if (!$options['show_deprecated']) { + $data['type_extensions'] = $options['extensions']; + $data['type_guessers'] = $options['guessers']; + } + + $this->writeData($data, $options); + } + + protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void + { + $this->collectOptions($resolvedFormType); + + if ($options['show_deprecated']) { + $this->filterOptionsByDeprecated($resolvedFormType); + } + + $formOptions = [ + 'own' => $this->ownOptions, + 'overridden' => $this->overriddenOptions, + 'parent' => $this->parentOptions, + 'extension' => $this->extensionOptions, + 'required' => $this->requiredOptions, + ]; + $this->sortOptions($formOptions); + + $data = [ + 'class' => $resolvedFormType->getInnerType()::class, + 'block_prefix' => $resolvedFormType->getInnerType()->getBlockPrefix(), + 'options' => $formOptions, + 'parent_types' => $this->parents, + 'type_extensions' => $this->extensions, + ]; + + $this->writeData($data, $options); + } + + protected function describeOption(OptionsResolver $optionsResolver, array $options): void + { + $definition = $this->getOptionDefinition($optionsResolver, $options['option']); + + $map = []; + if ($definition['deprecated']) { + $map['deprecated'] = 'deprecated'; + if (\is_string($definition['deprecationMessage'])) { + $map['deprecation_message'] = 'deprecationMessage'; + } + } + $map += [ + 'info' => 'info', + 'required' => 'required', + 'default' => 'default', + 'allowed_types' => 'allowedTypes', + 'allowed_values' => 'allowedValues', + ]; + foreach ($map as $label => $name) { + if (\array_key_exists($name, $definition)) { + $data[$label] = $definition[$name]; + + if ('default' === $name) { + $data['is_lazy'] = isset($definition['lazy']); + } + } + } + $data['has_normalizer'] = isset($definition['normalizers']); + + $this->writeData($data, $options); + } + + private function writeData(array $data, array $options): void + { + $flags = $options['json_encoding'] ?? 0; + + $this->output->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n"); + } + + private function sortOptions(array &$options): void + { + foreach ($options as &$opts) { + $sorted = false; + foreach ($opts as &$opt) { + if (\is_array($opt)) { + sort($opt); + $sorted = true; + } + } + if (!$sorted) { + sort($opts); + } + } + } +} diff --git a/lib/symfony/form/Console/Descriptor/TextDescriptor.php b/lib/symfony/form/Console/Descriptor/TextDescriptor.php new file mode 100644 index 0000000000..1b1ee9ec04 --- /dev/null +++ b/lib/symfony/form/Console/Descriptor/TextDescriptor.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Console\Descriptor; + +use Symfony\Component\Console\Helper\Dumper; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; +use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter as LegacyFileLinkFormatter; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Yonel Ceruto + * + * @internal + */ +class TextDescriptor extends Descriptor +{ + private FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter; + + public function __construct(FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter = null) + { + $this->fileLinkFormatter = $fileLinkFormatter; + } + + protected function describeDefaults(array $options): void + { + if ($options['core_types']) { + $this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)'); + $shortClassNames = array_map(fn ($fqcn) => $this->formatClassLink($fqcn, \array_slice(explode('\\', $fqcn), -1)[0]), $options['core_types']); + for ($i = 0, $loopsMax = \count($shortClassNames); $i * 5 < $loopsMax; ++$i) { + $this->output->writeln(' '.implode(', ', \array_slice($shortClassNames, $i * 5, 5))); + } + } + + if ($options['service_types']) { + $this->output->section('Service form types'); + $this->output->listing(array_map($this->formatClassLink(...), $options['service_types'])); + } + + if (!$options['show_deprecated']) { + if ($options['extensions']) { + $this->output->section('Type extensions'); + $this->output->listing(array_map($this->formatClassLink(...), $options['extensions'])); + } + + if ($options['guessers']) { + $this->output->section('Type guessers'); + $this->output->listing(array_map($this->formatClassLink(...), $options['guessers'])); + } + } + } + + protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void + { + $this->collectOptions($resolvedFormType); + + if ($options['show_deprecated']) { + $this->filterOptionsByDeprecated($resolvedFormType); + } + + $formOptions = $this->normalizeAndSortOptionsColumns(array_filter([ + 'own' => $this->ownOptions, + 'overridden' => $this->overriddenOptions, + 'parent' => $this->parentOptions, + 'extension' => $this->extensionOptions, + ])); + + // setting headers and column order + $tableHeaders = array_intersect_key([ + 'own' => 'Options', + 'overridden' => 'Overridden options', + 'parent' => 'Parent options', + 'extension' => 'Extension options', + ], $formOptions); + + $this->output->title(\sprintf('%s (Block prefix: "%s")', $resolvedFormType->getInnerType()::class, $resolvedFormType->getInnerType()->getBlockPrefix())); + + if ($formOptions) { + $this->output->table($tableHeaders, $this->buildTableRows($tableHeaders, $formOptions)); + } + + if ($this->parents) { + $this->output->section('Parent types'); + $this->output->listing(array_map($this->formatClassLink(...), $this->parents)); + } + + if ($this->extensions) { + $this->output->section('Type extensions'); + $this->output->listing(array_map($this->formatClassLink(...), $this->extensions)); + } + } + + protected function describeOption(OptionsResolver $optionsResolver, array $options): void + { + $definition = $this->getOptionDefinition($optionsResolver, $options['option']); + + $dump = new Dumper($this->output); + $map = []; + if ($definition['deprecated']) { + $map = [ + 'Deprecated' => 'deprecated', + 'Deprecation package' => 'deprecationPackage', + 'Deprecation version' => 'deprecationVersion', + 'Deprecation message' => 'deprecationMessage', + ]; + } + $map += [ + 'Info' => 'info', + 'Required' => 'required', + 'Default' => 'default', + 'Allowed types' => 'allowedTypes', + 'Allowed values' => 'allowedValues', + 'Normalizers' => 'normalizers', + ]; + $rows = []; + foreach ($map as $label => $name) { + $value = \array_key_exists($name, $definition) ? $dump($definition[$name]) : '-'; + if ('default' === $name && isset($definition['lazy'])) { + $value = "Value: $value\n\nClosure(s): ".$dump($definition['lazy']); + } + + $rows[] = ["$label", $value]; + $rows[] = new TableSeparator(); + } + array_pop($rows); + + $this->output->title(\sprintf('%s (%s)', $options['type']::class, $options['option'])); + $this->output->table([], $rows); + } + + private function buildTableRows(array $headers, array $options): array + { + $tableRows = []; + $count = \count(max($options)); + for ($i = 0; $i < $count; ++$i) { + $cells = []; + foreach (array_keys($headers) as $group) { + $option = $options[$group][$i] ?? null; + if (\is_string($option) && \in_array($option, $this->requiredOptions, true)) { + $option .= ' (required)'; + } + $cells[] = $option; + } + $tableRows[] = $cells; + } + + return $tableRows; + } + + private function normalizeAndSortOptionsColumns(array $options): array + { + foreach ($options as $group => $opts) { + $sorted = false; + foreach ($opts as $class => $opt) { + if (\is_string($class)) { + unset($options[$group][$class]); + } + + if (!\is_array($opt) || 0 === \count($opt)) { + continue; + } + + if (!$sorted) { + $options[$group] = []; + } else { + $options[$group][] = null; + } + $options[$group][] = \sprintf('%s', (new \ReflectionClass($class))->getShortName()); + $options[$group][] = new TableSeparator(); + + sort($opt); + $sorted = true; + $options[$group] = array_merge($options[$group], $opt); + } + + if (!$sorted) { + sort($options[$group]); + } + } + + return $options; + } + + private function formatClassLink(string $class, ?string $text = null): string + { + $text ??= $class; + + if ('' === $fileLink = $this->getFileLink($class)) { + return $text; + } + + return \sprintf('%s', $fileLink, $text); + } + + private function getFileLink(string $class): string + { + if (null === $this->fileLinkFormatter) { + return ''; + } + + try { + $r = new \ReflectionClass($class); + } catch (\ReflectionException) { + return ''; + } + + return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine()); + } +} diff --git a/lib/symfony/form/Console/Helper/DescriptorHelper.php b/lib/symfony/form/Console/Helper/DescriptorHelper.php new file mode 100644 index 0000000000..8f782ca6b0 --- /dev/null +++ b/lib/symfony/form/Console/Helper/DescriptorHelper.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\Form\Console\Helper; + +use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; +use Symfony\Component\Form\Console\Descriptor\JsonDescriptor; +use Symfony\Component\Form\Console\Descriptor\TextDescriptor; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter as LegacyFileLinkFormatter; + +/** + * @author Yonel Ceruto + * + * @internal + */ +class DescriptorHelper extends BaseDescriptorHelper +{ + public function __construct(FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter = null) + { + $this + ->register('txt', new TextDescriptor($fileLinkFormatter)) + ->register('json', new JsonDescriptor()) + ; + } +} diff --git a/lib/symfony/form/DataAccessorInterface.php b/lib/symfony/form/DataAccessorInterface.php new file mode 100644 index 0000000000..a0aea7e0ee --- /dev/null +++ b/lib/symfony/form/DataAccessorInterface.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * Writes and reads values to/from an object or array bound to a form. + * + * @author Yonel Ceruto + */ +interface DataAccessorInterface +{ + /** + * Returns the value at the end of the property of the object graph. + * + * @throws Exception\AccessException If unable to read from the given form data + */ + public function getValue(object|array $viewData, FormInterface $form): mixed; + + /** + * Sets the value at the end of the property of the object graph. + * + * @throws Exception\AccessException If unable to write the given value + */ + public function setValue(object|array &$viewData, mixed $value, FormInterface $form): void; + + /** + * Returns whether a value can be read from an object graph. + * + * Whenever this method returns true, {@link getValue()} is guaranteed not + * to throw an exception when called with the same arguments. + */ + public function isReadable(object|array $viewData, FormInterface $form): bool; + + /** + * Returns whether a value can be written at a given object graph. + * + * Whenever this method returns true, {@link setValue()} is guaranteed not + * to throw an exception when called with the same arguments. + */ + public function isWritable(object|array $viewData, FormInterface $form): bool; +} diff --git a/lib/symfony/form/DataMapperInterface.php b/lib/symfony/form/DataMapperInterface.php new file mode 100644 index 0000000000..f04137aec6 --- /dev/null +++ b/lib/symfony/form/DataMapperInterface.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @author Bernhard Schussek + */ +interface DataMapperInterface +{ + /** + * Maps the view data of a compound form to its children. + * + * The method is responsible for calling {@link FormInterface::setData()} + * on the children of compound forms, defining their underlying model data. + * + * @param mixed $viewData View data of the compound form being initialized + * @param \Traversable $forms A list of {@link FormInterface} instances + * + * @return void + * + * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported + */ + public function mapDataToForms(mixed $viewData, \Traversable $forms); + + /** + * Maps the model data of a list of children forms into the view data of their parent. + * + * This is the internal cascade call of FormInterface::submit for compound forms, since they + * cannot be bound to any input nor the request as scalar, but their children may: + * + * $compoundForm->submit($arrayOfChildrenViewData) + * // inside: + * $childForm->submit($childViewData); + * // for each entry, do the same and/or reverse transform + * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData) + * // then reverse transform + * + * When a simple form is submitted the following is happening: + * + * $simpleForm->submit($submittedViewData) + * // inside: + * $this->viewData = $submittedViewData + * // then reverse transform + * + * The model data can be an array or an object, so this second argument is always passed + * by reference. + * + * @param \Traversable $forms A list of {@link FormInterface} instances + * @param mixed &$viewData The compound form's view data that get mapped + * its children model data + * + * @return void + * + * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported + */ + public function mapFormsToData(\Traversable $forms, mixed &$viewData); +} diff --git a/lib/symfony/form/DataTransformerInterface.php b/lib/symfony/form/DataTransformerInterface.php new file mode 100644 index 0000000000..85fb99d218 --- /dev/null +++ b/lib/symfony/form/DataTransformerInterface.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms a value between different representations. + * + * @author Bernhard Schussek + * + * @template TValue + * @template TTransformedValue + */ +interface DataTransformerInterface +{ + /** + * Transforms a value from the original representation to a transformed representation. + * + * This method is called when the form field is initialized with its default data, on + * two occasions for two types of transformers: + * + * 1. Model transformers which normalize the model data. + * This is mainly useful when the same form type (the same configuration) + * has to handle different kind of underlying data, e.g The DateType can + * deal with strings or \DateTime objects as input. + * + * 2. View transformers which adapt the normalized data to the view format. + * a/ When the form is simple, the value returned by convention is used + * directly in the view and thus can only be a string or an array. In + * this case the data class should be null. + * + * b/ When the form is compound the returned value should be an array or + * an object to be mapped to the children. Each property of the compound + * data will be used as model data by each child and will be transformed + * too. In this case data class should be the class of the object, or null + * when it is an array. + * + * All transformers are called in a configured order from model data to view value. + * At the end of this chain the view data will be validated against the data class + * setting. + * + * This method must be able to deal with empty values. Usually this will + * be NULL, but depending on your implementation other empty values are + * possible as well (such as empty strings). The reasoning behind this is + * that data transformers must be chainable. If the transform() method + * of the first data transformer outputs NULL, the second must be able to + * process that value. + * + * @param TValue|null $value The value in the original representation + * + * @return mixed + * + * @psalm-return TTransformedValue|null + * + * @throws TransformationFailedException when the transformation fails + */ + public function transform(mixed $value); + + /** + * Transforms a value from the transformed representation to its original + * representation. + * + * This method is called when {@link Form::submit()} is called to transform the requests tainted data + * into an acceptable format. + * + * The same transformers are called in the reverse order so the responsibility is to + * return one of the types that would be expected as input of transform(). + * + * This method must be able to deal with empty values. Usually this will + * be an empty string, but depending on your implementation other empty + * values are possible as well (such as NULL). The reasoning behind + * this is that value transformers must be chainable. If the + * reverseTransform() method of the first value transformer outputs an + * empty string, the second value transformer must be able to process that + * value. + * + * By convention, reverseTransform() should return NULL if an empty string + * is passed. + * + * @param TTransformedValue|null $value The value in the transformed representation + * + * @return mixed + * + * @psalm-return TValue|null + * + * @throws TransformationFailedException when the transformation fails + */ + public function reverseTransform(mixed $value); +} diff --git a/lib/symfony/form/DependencyInjection/FormPass.php b/lib/symfony/form/DependencyInjection/FormPass.php new file mode 100644 index 0000000000..4b9a53353b --- /dev/null +++ b/lib/symfony/form/DependencyInjection/FormPass.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds all services with the tags "form.type", "form.type_extension" and + * "form.type_guesser" as arguments of the "form.extension" service. + * + * @author Bernhard Schussek + */ +class FormPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('form.extension')) { + return; + } + + $definition = $container->getDefinition('form.extension'); + $definition->replaceArgument(0, $this->processFormTypes($container)); + $definition->replaceArgument(1, $this->processFormTypeExtensions($container)); + $definition->replaceArgument(2, $this->processFormTypeGuessers($container)); + } + + private function processFormTypes(ContainerBuilder $container): Reference + { + // Get service locator argument + $servicesMap = []; + $namespaces = ['Symfony\Component\Form\Extension\Core\Type' => true]; + + // Builds an array with fully-qualified type class names as keys and service IDs as values + foreach ($container->findTaggedServiceIds('form.type', true) as $serviceId => $tag) { + // Add form type service to the service locator + $serviceDefinition = $container->getDefinition($serviceId); + $servicesMap[$formType = $serviceDefinition->getClass()] = new Reference($serviceId); + $namespaces[substr($formType, 0, strrpos($formType, '\\'))] = true; + } + + if ($container->hasDefinition('console.command.form_debug')) { + $commandDefinition = $container->getDefinition('console.command.form_debug'); + $commandDefinition->setArgument(1, array_keys($namespaces)); + $commandDefinition->setArgument(2, array_keys($servicesMap)); + } + + return ServiceLocatorTagPass::register($container, $servicesMap); + } + + private function processFormTypeExtensions(ContainerBuilder $container): array + { + $typeExtensions = []; + $typeExtensionsClasses = []; + foreach ($this->findAndSortTaggedServices('form.type_extension', $container) as $reference) { + $serviceId = (string) $reference; + $serviceDefinition = $container->getDefinition($serviceId); + + $tag = $serviceDefinition->getTag('form.type_extension'); + $typeExtensionClass = $container->getParameterBag()->resolveValue($serviceDefinition->getClass()); + + if (isset($tag[0]['extended_type'])) { + $typeExtensions[$tag[0]['extended_type']][] = new Reference($serviceId); + $typeExtensionsClasses[] = $typeExtensionClass; + } else { + $extendsTypes = false; + + $typeExtensionsClasses[] = $typeExtensionClass; + foreach ($typeExtensionClass::getExtendedTypes() as $extendedType) { + $typeExtensions[$extendedType][] = new Reference($serviceId); + $extendsTypes = true; + } + + if (!$extendsTypes) { + throw new InvalidArgumentException(\sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId)); + } + } + } + + foreach ($typeExtensions as $extendedType => $extensions) { + $typeExtensions[$extendedType] = new IteratorArgument($extensions); + } + + if ($container->hasDefinition('console.command.form_debug')) { + $commandDefinition = $container->getDefinition('console.command.form_debug'); + $commandDefinition->setArgument(3, $typeExtensionsClasses); + } + + return $typeExtensions; + } + + private function processFormTypeGuessers(ContainerBuilder $container): ArgumentInterface + { + $guessers = []; + $guessersClasses = []; + foreach ($container->findTaggedServiceIds('form.type_guesser', true) as $serviceId => $tags) { + $guessers[] = new Reference($serviceId); + + $serviceDefinition = $container->getDefinition($serviceId); + $guessersClasses[] = $serviceDefinition->getClass(); + } + + if ($container->hasDefinition('console.command.form_debug')) { + $commandDefinition = $container->getDefinition('console.command.form_debug'); + $commandDefinition->setArgument(4, $guessersClasses); + } + + return new IteratorArgument($guessers); + } +} diff --git a/lib/symfony/form/Event/PostSetDataEvent.php b/lib/symfony/form/Event/PostSetDataEvent.php new file mode 100644 index 0000000000..7d551f8b52 --- /dev/null +++ b/lib/symfony/form/Event/PostSetDataEvent.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched at the end of the Form::setData() method. + * + * It can be used to modify a form depending on the populated data (adding or + * removing fields dynamically). + */ +final class PostSetDataEvent extends FormEvent +{ + /** + * @deprecated since Symfony 6.4, it will throw an exception in 7.0. + */ + public function setData(mixed $data): void + { + trigger_deprecation('symfony/form', '6.4', 'Calling "%s()" will throw an exception as of 7.0, listen to "form.pre_set_data" instead.', __METHOD__); + // throw new BadMethodCallException('Form data cannot be changed during "form.post_set_data", you should use "form.pre_set_data" instead.'); + parent::setData($data); + } +} diff --git a/lib/symfony/form/Event/PostSubmitEvent.php b/lib/symfony/form/Event/PostSubmitEvent.php new file mode 100644 index 0000000000..5ce6d8ecb7 --- /dev/null +++ b/lib/symfony/form/Event/PostSubmitEvent.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched after the Form::submit() + * once the model and view data have been denormalized. + * + * It can be used to fetch data after denormalization. + */ +final class PostSubmitEvent extends FormEvent +{ + /** + * @deprecated since Symfony 6.4, it will throw an exception in 7.0. + */ + public function setData(mixed $data): void + { + trigger_deprecation('symfony/form', '6.4', 'Calling "%s()" will throw an exception as of 7.0, listen to "form.pre_submit" or "form.submit" instead.', __METHOD__); + // throw new BadMethodCallException('Form data cannot be changed during "form.post_submit", you should use "form.pre_submit" or "form.submit" instead.'); + parent::setData($data); + } +} diff --git a/lib/symfony/form/Event/PreSetDataEvent.php b/lib/symfony/form/Event/PreSetDataEvent.php new file mode 100644 index 0000000000..2644fda19c --- /dev/null +++ b/lib/symfony/form/Event/PreSetDataEvent.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\Form\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched at the beginning of the Form::setData() method. + * + * It can be used to modify the data given during pre-population. + */ +final class PreSetDataEvent extends FormEvent +{ +} diff --git a/lib/symfony/form/Event/PreSubmitEvent.php b/lib/symfony/form/Event/PreSubmitEvent.php new file mode 100644 index 0000000000..a72ac5d160 --- /dev/null +++ b/lib/symfony/form/Event/PreSubmitEvent.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\Form\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched at the beginning of the Form::submit() method. + * + * It can be used to: + * - Change data from the request, before submitting the data to the form. + * - Add or remove form fields, before submitting the data to the form. + */ +final class PreSubmitEvent extends FormEvent +{ +} diff --git a/lib/symfony/form/Event/SubmitEvent.php b/lib/symfony/form/Event/SubmitEvent.php new file mode 100644 index 0000000000..71d3b06d47 --- /dev/null +++ b/lib/symfony/form/Event/SubmitEvent.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\Form\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched just before the Form::submit() method + * transforms back the normalized data to the model and view data. + * + * It can be used to change data from the normalized representation of the data. + */ +final class SubmitEvent extends FormEvent +{ +} diff --git a/lib/symfony/form/Exception/AccessException.php b/lib/symfony/form/Exception/AccessException.php new file mode 100644 index 0000000000..ac712cc3d4 --- /dev/null +++ b/lib/symfony/form/Exception/AccessException.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\Form\Exception; + +class AccessException extends RuntimeException +{ +} diff --git a/lib/symfony/form/Exception/AlreadySubmittedException.php b/lib/symfony/form/Exception/AlreadySubmittedException.php new file mode 100644 index 0000000000..5e8c305262 --- /dev/null +++ b/lib/symfony/form/Exception/AlreadySubmittedException.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\Form\Exception; + +/** + * Thrown when an operation is called that is not acceptable after submitting + * a form. + * + * @author Bernhard Schussek + */ +class AlreadySubmittedException extends LogicException +{ +} diff --git a/lib/symfony/form/Exception/BadMethodCallException.php b/lib/symfony/form/Exception/BadMethodCallException.php new file mode 100644 index 0000000000..27649dd022 --- /dev/null +++ b/lib/symfony/form/Exception/BadMethodCallException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Base BadMethodCallException for the Form component. + * + * @author Bernhard Schussek + */ +class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface +{ +} diff --git a/lib/symfony/form/Exception/ErrorMappingException.php b/lib/symfony/form/Exception/ErrorMappingException.php new file mode 100644 index 0000000000..a696849264 --- /dev/null +++ b/lib/symfony/form/Exception/ErrorMappingException.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\Form\Exception; + +class ErrorMappingException extends RuntimeException +{ +} diff --git a/lib/symfony/form/Exception/ExceptionInterface.php b/lib/symfony/form/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..69145f0bcd --- /dev/null +++ b/lib/symfony/form/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Base ExceptionInterface for the Form component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/lib/symfony/form/Exception/InvalidArgumentException.php b/lib/symfony/form/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..a270e0ce9e --- /dev/null +++ b/lib/symfony/form/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Base InvalidArgumentException for the Form component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/lib/symfony/form/Exception/InvalidConfigurationException.php b/lib/symfony/form/Exception/InvalidConfigurationException.php new file mode 100644 index 0000000000..daa0c42f58 --- /dev/null +++ b/lib/symfony/form/Exception/InvalidConfigurationException.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\Form\Exception; + +class InvalidConfigurationException extends InvalidArgumentException +{ +} diff --git a/lib/symfony/form/Exception/LogicException.php b/lib/symfony/form/Exception/LogicException.php new file mode 100644 index 0000000000..848780215b --- /dev/null +++ b/lib/symfony/form/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Base LogicException for Form component. + * + * @author Alexander Kotynia + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/lib/symfony/form/Exception/OutOfBoundsException.php b/lib/symfony/form/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000..44d3116630 --- /dev/null +++ b/lib/symfony/form/Exception/OutOfBoundsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Base OutOfBoundsException for Form component. + * + * @author Alexander Kotynia + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/lib/symfony/form/Exception/RuntimeException.php b/lib/symfony/form/Exception/RuntimeException.php new file mode 100644 index 0000000000..0af48a4a21 --- /dev/null +++ b/lib/symfony/form/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Base RuntimeException for the Form component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/lib/symfony/form/Exception/StringCastException.php b/lib/symfony/form/Exception/StringCastException.php new file mode 100644 index 0000000000..f9b51d6049 --- /dev/null +++ b/lib/symfony/form/Exception/StringCastException.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\Form\Exception; + +class StringCastException extends RuntimeException +{ +} diff --git a/lib/symfony/form/Exception/TransformationFailedException.php b/lib/symfony/form/Exception/TransformationFailedException.php new file mode 100644 index 0000000000..8388a0ba64 --- /dev/null +++ b/lib/symfony/form/Exception/TransformationFailedException.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Indicates a value transformation error. + * + * @author Bernhard Schussek + */ +class TransformationFailedException extends RuntimeException +{ + private ?string $invalidMessage; + private array $invalidMessageParameters; + + public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, ?string $invalidMessage = null, array $invalidMessageParameters = []) + { + parent::__construct($message, $code, $previous); + + $this->setInvalidMessage($invalidMessage, $invalidMessageParameters); + } + + /** + * Sets the message that will be shown to the user. + * + * @param string|null $invalidMessage The message or message key + * @param array $invalidMessageParameters Data to be passed into the translator + */ + public function setInvalidMessage(?string $invalidMessage = null, array $invalidMessageParameters = []): void + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/form', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->invalidMessage = $invalidMessage; + $this->invalidMessageParameters = $invalidMessageParameters; + } + + public function getInvalidMessage(): ?string + { + return $this->invalidMessage; + } + + public function getInvalidMessageParameters(): array + { + return $this->invalidMessageParameters; + } +} diff --git a/lib/symfony/form/Exception/UnexpectedTypeException.php b/lib/symfony/form/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000000..223061b77b --- /dev/null +++ b/lib/symfony/form/Exception/UnexpectedTypeException.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\Form\Exception; + +class UnexpectedTypeException extends InvalidArgumentException +{ + public function __construct(mixed $value, string $expectedType) + { + parent::__construct(\sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value))); + } +} diff --git a/lib/symfony/form/Extension/Core/CoreExtension.php b/lib/symfony/form/Extension/Core/CoreExtension.php new file mode 100644 index 0000000000..d6c3ff080a --- /dev/null +++ b/lib/symfony/form/Extension/Core/CoreExtension.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Represents the main form extension, which loads the core functionality. + * + * @author Bernhard Schussek + */ +class CoreExtension extends AbstractExtension +{ + private PropertyAccessorInterface $propertyAccessor; + private ChoiceListFactoryInterface $choiceListFactory; + private ?TranslatorInterface $translator; + + public function __construct(?PropertyAccessorInterface $propertyAccessor = null, ?ChoiceListFactoryInterface $choiceListFactory = null, ?TranslatorInterface $translator = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); + $this->translator = $translator; + } + + protected function loadTypes(): array + { + return [ + new Type\FormType($this->propertyAccessor), + new Type\BirthdayType(), + new Type\CheckboxType(), + new Type\ChoiceType($this->choiceListFactory, $this->translator), + new Type\CollectionType(), + new Type\CountryType(), + new Type\DateIntervalType(), + new Type\DateType(), + new Type\DateTimeType(), + new Type\EmailType(), + new Type\HiddenType(), + new Type\IntegerType(), + new Type\LanguageType(), + new Type\LocaleType(), + new Type\MoneyType(), + new Type\NumberType(), + new Type\PasswordType(), + new Type\PercentType(), + new Type\RadioType(), + new Type\RangeType(), + new Type\RepeatedType(), + new Type\SearchType(), + new Type\TextareaType(), + new Type\TextType(), + new Type\TimeType(), + new Type\TimezoneType(), + new Type\UrlType(), + new Type\FileType($this->translator), + new Type\ButtonType(), + new Type\SubmitType(), + new Type\ResetType(), + new Type\CurrencyType(), + new Type\TelType(), + new Type\ColorType($this->translator), + new Type\WeekType(), + ]; + } + + protected function loadTypeExtensions(): array + { + return [ + new TransformationFailureExtension($this->translator), + ]; + } +} diff --git a/lib/symfony/form/Extension/Core/DataAccessor/CallbackAccessor.php b/lib/symfony/form/Extension/Core/DataAccessor/CallbackAccessor.php new file mode 100644 index 0000000000..a7d5bb13fe --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataAccessor/CallbackAccessor.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataAccessor; + +use Symfony\Component\Form\DataAccessorInterface; +use Symfony\Component\Form\Exception\AccessException; +use Symfony\Component\Form\FormInterface; + +/** + * Writes and reads values to/from an object or array using callback functions. + * + * @author Yonel Ceruto + */ +class CallbackAccessor implements DataAccessorInterface +{ + public function getValue(object|array $data, FormInterface $form): mixed + { + if (null === $getter = $form->getConfig()->getOption('getter')) { + throw new AccessException('Unable to read from the given form data as no getter is defined.'); + } + + return ($getter)($data, $form); + } + + public function setValue(object|array &$data, mixed $value, FormInterface $form): void + { + if (null === $setter = $form->getConfig()->getOption('setter')) { + throw new AccessException('Unable to write the given value as no setter is defined.'); + } + + ($setter)($data, $form->getData(), $form); + } + + public function isReadable(object|array $data, FormInterface $form): bool + { + return null !== $form->getConfig()->getOption('getter'); + } + + public function isWritable(object|array $data, FormInterface $form): bool + { + return null !== $form->getConfig()->getOption('setter'); + } +} diff --git a/lib/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php b/lib/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php new file mode 100644 index 0000000000..ac600f16f0 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataAccessor; + +use Symfony\Component\Form\DataAccessorInterface; +use Symfony\Component\Form\Exception\AccessException; +use Symfony\Component\Form\FormInterface; + +/** + * @author Yonel Ceruto + */ +class ChainAccessor implements DataAccessorInterface +{ + private iterable $accessors; + + /** + * @param DataAccessorInterface[]|iterable $accessors + */ + public function __construct(iterable $accessors) + { + $this->accessors = $accessors; + } + + public function getValue(object|array $data, FormInterface $form): mixed + { + foreach ($this->accessors as $accessor) { + if ($accessor->isReadable($data, $form)) { + return $accessor->getValue($data, $form); + } + } + + throw new AccessException('Unable to read from the given form data as no accessor in the chain is able to read the data.'); + } + + public function setValue(object|array &$data, mixed $value, FormInterface $form): void + { + foreach ($this->accessors as $accessor) { + if ($accessor->isWritable($data, $form)) { + $accessor->setValue($data, $value, $form); + + return; + } + } + + throw new AccessException('Unable to write the given value as no accessor in the chain is able to set the data.'); + } + + public function isReadable(object|array $data, FormInterface $form): bool + { + foreach ($this->accessors as $accessor) { + if ($accessor->isReadable($data, $form)) { + return true; + } + } + + return false; + } + + public function isWritable(object|array $data, FormInterface $form): bool + { + foreach ($this->accessors as $accessor) { + if ($accessor->isWritable($data, $form)) { + return true; + } + } + + return false; + } +} diff --git a/lib/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php b/lib/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php new file mode 100644 index 0000000000..f5c25dfc1c --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataAccessor; + +use Symfony\Component\Form\DataAccessorInterface; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\AccessException; +use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * Writes and reads values to/from an object or array using property path. + * + * @author Yonel Ceruto + * @author Bernhard Schussek + */ +class PropertyPathAccessor implements DataAccessorInterface +{ + private PropertyAccessorInterface $propertyAccessor; + + public function __construct(?PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); + } + + public function getValue(object|array $data, FormInterface $form): mixed + { + if (null === $propertyPath = $form->getPropertyPath()) { + throw new AccessException('Unable to read from the given form data as no property path is defined.'); + } + + return $this->getPropertyValue($data, $propertyPath); + } + + public function setValue(object|array &$data, mixed $value, FormInterface $form): void + { + if (null === $propertyPath = $form->getPropertyPath()) { + throw new AccessException('Unable to write the given value as no property path is defined.'); + } + + $getValue = function () use ($data, $form, $propertyPath) { + $dataMapper = $this->getDataMapper($form); + + if ($dataMapper instanceof DataMapper && null !== $dataAccessor = $dataMapper->getDataAccessor()) { + return $dataAccessor->getValue($data, $form); + } + + return $this->getPropertyValue($data, $propertyPath); + }; + + // If the field is of type DateTimeInterface and the data is the same skip the update to + // keep the original object hash + if ($value instanceof \DateTimeInterface && $value == $getValue()) { + return; + } + + // If the data is identical to the value in $data, we are + // dealing with a reference + if (!\is_object($data) || !$form->getConfig()->getByReference() || $value !== $getValue()) { + $this->propertyAccessor->setValue($data, $propertyPath, $value); + } + } + + public function isReadable(object|array $data, FormInterface $form): bool + { + return null !== $form->getPropertyPath(); + } + + public function isWritable(object|array $data, FormInterface $form): bool + { + return null !== $form->getPropertyPath(); + } + + private function getPropertyValue(object|array $data, PropertyPathInterface $propertyPath): mixed + { + try { + return $this->propertyAccessor->getValue($data, $propertyPath); + } catch (PropertyAccessException $e) { + if (\is_array($data) && $e instanceof NoSuchIndexException) { + return null; + } + + if (!$e instanceof UninitializedPropertyException + // For versions without UninitializedPropertyException check the exception message + && (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it')) + ) { + throw $e; + } + + return null; + } + } + + private function getDataMapper(FormInterface $form): ?DataMapperInterface + { + do { + $dataMapper = $form->getConfig()->getDataMapper(); + } while (null === $dataMapper && null !== $form = $form->getParent()); + + return $dataMapper; + } +} diff --git a/lib/symfony/form/Extension/Core/DataMapper/CheckboxListMapper.php b/lib/symfony/form/Extension/Core/DataMapper/CheckboxListMapper.php new file mode 100644 index 0000000000..119c81107d --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataMapper/CheckboxListMapper.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataMapper; + +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Maps choices to/from checkbox forms. + * + * A {@link ChoiceListInterface} implementation is used to find the + * corresponding string values for the choices. Each checkbox form whose "value" + * option corresponds to any of the selected values is marked as selected. + * + * @author Bernhard Schussek + */ +class CheckboxListMapper implements DataMapperInterface +{ + /** + * @return void + */ + public function mapDataToForms(mixed $choices, \Traversable $checkboxes) + { + if (!\is_array($choices ??= [])) { + throw new UnexpectedTypeException($choices, 'array'); + } + + foreach ($checkboxes as $checkbox) { + $value = $checkbox->getConfig()->getOption('value'); + $checkbox->setData(\in_array($value, $choices, true)); + } + } + + /** + * @return void + */ + public function mapFormsToData(\Traversable $checkboxes, mixed &$choices) + { + if (!\is_array($choices)) { + throw new UnexpectedTypeException($choices, 'array'); + } + + $values = []; + + foreach ($checkboxes as $checkbox) { + if ($checkbox->getData()) { + // construct an array of choice values + $values[] = $checkbox->getConfig()->getOption('value'); + } + } + + $choices = $values; + } +} diff --git a/lib/symfony/form/Extension/Core/DataMapper/DataMapper.php b/lib/symfony/form/Extension/Core/DataMapper/DataMapper.php new file mode 100644 index 0000000000..a7bf980322 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataMapper/DataMapper.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataMapper; + +use Symfony\Component\Form\DataAccessorInterface; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor; +use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor; +use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; + +/** + * Maps arrays/objects to/from forms using data accessors. + * + * @author Bernhard Schussek + */ +class DataMapper implements DataMapperInterface +{ + private DataAccessorInterface $dataAccessor; + + public function __construct(?DataAccessorInterface $dataAccessor = null) + { + $this->dataAccessor = $dataAccessor ?? new ChainAccessor([ + new CallbackAccessor(), + new PropertyPathAccessor(), + ]); + } + + public function mapDataToForms(mixed $data, \Traversable $forms): void + { + $empty = null === $data || [] === $data; + + if (!$empty && !\is_array($data) && !\is_object($data)) { + throw new UnexpectedTypeException($data, 'object, array or empty'); + } + + foreach ($forms as $form) { + $config = $form->getConfig(); + + if (!$empty && $config->getMapped() && $this->dataAccessor->isReadable($data, $form)) { + $form->setData($this->dataAccessor->getValue($data, $form)); + } else { + $form->setData($config->getData()); + } + } + } + + public function mapFormsToData(\Traversable $forms, mixed &$data): void + { + if (null === $data) { + return; + } + + if (!\is_array($data) && !\is_object($data)) { + throw new UnexpectedTypeException($data, 'object, array or empty'); + } + + foreach ($forms as $form) { + $config = $form->getConfig(); + + // Write-back is disabled if the form is not synchronized (transformation failed), + // if the form was not submitted and if the form is disabled (modification not allowed) + if ($config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled() && $this->dataAccessor->isWritable($data, $form)) { + $this->dataAccessor->setValue($data, $form->getData(), $form); + } + } + } + + /** + * @internal + */ + public function getDataAccessor(): DataAccessorInterface + { + return $this->dataAccessor; + } +} diff --git a/lib/symfony/form/Extension/Core/DataMapper/RadioListMapper.php b/lib/symfony/form/Extension/Core/DataMapper/RadioListMapper.php new file mode 100644 index 0000000000..37fdba0c35 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataMapper/RadioListMapper.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataMapper; + +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Maps choices to/from radio forms. + * + * A {@link ChoiceListInterface} implementation is used to find the + * corresponding string values for the choices. The radio form whose "value" + * option corresponds to the selected value is marked as selected. + * + * @author Bernhard Schussek + */ +class RadioListMapper implements DataMapperInterface +{ + /** + * @return void + */ + public function mapDataToForms(mixed $choice, \Traversable $radios) + { + if (!\is_string($choice)) { + throw new UnexpectedTypeException($choice, 'string'); + } + + foreach ($radios as $radio) { + $value = $radio->getConfig()->getOption('value'); + $radio->setData($choice === $value); + } + } + + /** + * @return void + */ + public function mapFormsToData(\Traversable $radios, mixed &$choice) + { + if (null !== $choice && !\is_string($choice)) { + throw new UnexpectedTypeException($choice, 'null or string'); + } + + $choice = null; + + foreach ($radios as $radio) { + if ($radio->getData()) { + if ('placeholder' === $radio->getName()) { + return; + } + + $choice = $radio->getConfig()->getOption('value'); + + return; + } + } + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/ArrayToPartsTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/ArrayToPartsTransformer.php new file mode 100644 index 0000000000..b35552894e --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/ArrayToPartsTransformer.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @author Bernhard Schussek + * + * @implements DataTransformerInterface + */ +class ArrayToPartsTransformer implements DataTransformerInterface +{ + private array $partMapping; + + public function __construct(array $partMapping) + { + $this->partMapping = $partMapping; + } + + public function transform(mixed $array): mixed + { + if (!\is_array($array ??= [])) { + throw new TransformationFailedException('Expected an array.'); + } + + $result = []; + + foreach ($this->partMapping as $partKey => $originalKeys) { + if (!$array) { + $result[$partKey] = null; + } else { + $result[$partKey] = array_intersect_key($array, array_flip($originalKeys)); + } + } + + return $result; + } + + public function reverseTransform(mixed $array): mixed + { + if (!\is_array($array)) { + throw new TransformationFailedException('Expected an array.'); + } + + $result = []; + $emptyKeys = []; + + foreach ($this->partMapping as $partKey => $originalKeys) { + if (!empty($array[$partKey])) { + foreach ($originalKeys as $originalKey) { + if (isset($array[$partKey][$originalKey])) { + $result[$originalKey] = $array[$partKey][$originalKey]; + } + } + } else { + $emptyKeys[] = $partKey; + } + } + + if (\count($emptyKeys) > 0) { + if (\count($emptyKeys) === \count($this->partMapping)) { + // All parts empty + return null; + } + + throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys))); + } + + return $result; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/BaseDateTimeTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/BaseDateTimeTransformer.php new file mode 100644 index 0000000000..902fc604f3 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/BaseDateTimeTransformer.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * @template TTransformedValue + * + * @implements DataTransformerInterface<\DateTimeInterface, TTransformedValue> + */ +abstract class BaseDateTimeTransformer implements DataTransformerInterface +{ + protected static $formats = [ + \IntlDateFormatter::NONE, + \IntlDateFormatter::FULL, + \IntlDateFormatter::LONG, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::SHORT, + ]; + + protected $inputTimezone; + + protected $outputTimezone; + + /** + * @param string|null $inputTimezone The name of the input timezone + * @param string|null $outputTimezone The name of the output timezone + * + * @throws InvalidArgumentException if a timezone is not valid + */ + public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null) + { + $this->inputTimezone = $inputTimezone ?: date_default_timezone_get(); + $this->outputTimezone = $outputTimezone ?: date_default_timezone_get(); + + // Check if input and output timezones are valid + try { + new \DateTimeZone($this->inputTimezone); + } catch (\Exception $e) { + throw new InvalidArgumentException(\sprintf('Input timezone is invalid: "%s".', $this->inputTimezone), $e->getCode(), $e); + } + + try { + new \DateTimeZone($this->outputTimezone); + } catch (\Exception $e) { + throw new InvalidArgumentException(\sprintf('Output timezone is invalid: "%s".', $this->outputTimezone), $e->getCode(), $e); + } + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/BooleanToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/BooleanToStringTransformer.php new file mode 100644 index 0000000000..e91bdb4dbf --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/BooleanToStringTransformer.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a Boolean and a string. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @implements DataTransformerInterface + */ +class BooleanToStringTransformer implements DataTransformerInterface +{ + private string $trueValue; + + private array $falseValues; + + /** + * @param string $trueValue The value emitted upon transform if the input is true + */ + public function __construct(string $trueValue, array $falseValues = [null]) + { + $this->trueValue = $trueValue; + $this->falseValues = $falseValues; + if (\in_array($this->trueValue, $this->falseValues, true)) { + throw new InvalidArgumentException('The specified "true" value is contained in the false-values.'); + } + } + + /** + * Transforms a Boolean into a string. + * + * @param bool $value Boolean value + * + * @throws TransformationFailedException if the given value is not a Boolean + */ + public function transform(mixed $value): ?string + { + if (null === $value) { + return null; + } + + if (!\is_bool($value)) { + throw new TransformationFailedException('Expected a Boolean.'); + } + + return $value ? $this->trueValue : null; + } + + /** + * Transforms a string into a Boolean. + * + * @param string $value String value + * + * @throws TransformationFailedException if the given value is not a string + */ + public function reverseTransform(mixed $value): bool + { + if (\in_array($value, $this->falseValues, true)) { + return false; + } + + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + return true; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php new file mode 100644 index 0000000000..a7322983c2 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @author Bernhard Schussek + * + * @implements DataTransformerInterface + */ +class ChoiceToValueTransformer implements DataTransformerInterface +{ + private ChoiceListInterface $choiceList; + + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + public function transform(mixed $choice): mixed + { + return (string) current($this->choiceList->getValuesForChoices([$choice])); + } + + public function reverseTransform(mixed $value): mixed + { + if (null !== $value && !\is_string($value)) { + throw new TransformationFailedException('Expected a string or null.'); + } + + $choices = $this->choiceList->getChoicesForValues([(string) $value]); + + if (1 !== \count($choices)) { + if (null === $value || '' === $value) { + return null; + } + + throw new TransformationFailedException(\sprintf('The choice "%s" does not exist or is not unique.', $value)); + } + + return current($choices); + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php new file mode 100644 index 0000000000..f284ff34f9 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @author Bernhard Schussek + * + * @implements DataTransformerInterface + */ +class ChoicesToValuesTransformer implements DataTransformerInterface +{ + private ChoiceListInterface $choiceList; + + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + /** + * @throws TransformationFailedException if the given value is not an array + */ + public function transform(mixed $array): array + { + if (null === $array) { + return []; + } + + if (!\is_array($array)) { + throw new TransformationFailedException('Expected an array.'); + } + + return $this->choiceList->getValuesForChoices($array); + } + + /** + * @throws TransformationFailedException if the given value is not an array + * or if no matching choice could be + * found for some given value + */ + public function reverseTransform(mixed $array): array + { + if (null === $array) { + return []; + } + + if (!\is_array($array)) { + throw new TransformationFailedException('Expected an array.'); + } + + $choices = $this->choiceList->getChoicesForValues($array); + + if (\count($choices) !== \count($array)) { + throw new TransformationFailedException('Could not find all matching choices for the given values.'); + } + + return $choices; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DataTransformerChain.php b/lib/symfony/form/Extension/Core/DataTransformer/DataTransformerChain.php new file mode 100644 index 0000000000..41b93e56a7 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DataTransformerChain.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Passes a value through multiple value transformers. + * + * @author Bernhard Schussek + */ +class DataTransformerChain implements DataTransformerInterface +{ + protected $transformers; + + /** + * Uses the given value transformers to transform values. + * + * @param DataTransformerInterface[] $transformers + */ + public function __construct(array $transformers) + { + $this->transformers = $transformers; + } + + /** + * Passes the value through the transform() method of all nested transformers. + * + * The transformers receive the value in the same order as they were passed + * to the constructor. Each transformer receives the result of the previous + * transformer as input. The output of the last transformer is returned + * by this method. + * + * @param mixed $value The original value + * + * @throws TransformationFailedException + */ + public function transform(mixed $value): mixed + { + foreach ($this->transformers as $transformer) { + $value = $transformer->transform($value); + } + + return $value; + } + + /** + * Passes the value through the reverseTransform() method of all nested + * transformers. + * + * The transformers receive the value in the reverse order as they were passed + * to the constructor. Each transformer receives the result of the previous + * transformer as input. The output of the last transformer is returned + * by this method. + * + * @param mixed $value The transformed value + * + * @throws TransformationFailedException + */ + public function reverseTransform(mixed $value): mixed + { + for ($i = \count($this->transformers) - 1; $i >= 0; --$i) { + $value = $this->transformers[$i]->reverseTransform($value); + } + + return $value; + } + + /** + * @return DataTransformerInterface[] + */ + public function getTransformers(): array + { + return $this->transformers; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php new file mode 100644 index 0000000000..af4dc6e322 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Transforms between a normalized date interval and an interval string/array. + * + * @author Steffen Roßkamp + * + * @implements DataTransformerInterface<\DateInterval, array> + */ +class DateIntervalToArrayTransformer implements DataTransformerInterface +{ + public const YEARS = 'years'; + public const MONTHS = 'months'; + public const DAYS = 'days'; + public const HOURS = 'hours'; + public const MINUTES = 'minutes'; + public const SECONDS = 'seconds'; + public const INVERT = 'invert'; + + private const AVAILABLE_FIELDS = [ + self::YEARS => 'y', + self::MONTHS => 'm', + self::DAYS => 'd', + self::HOURS => 'h', + self::MINUTES => 'i', + self::SECONDS => 's', + self::INVERT => 'r', + ]; + private array $fields; + private bool $pad; + + /** + * @param string[]|null $fields The date fields + * @param bool $pad Whether to use padding + */ + public function __construct(?array $fields = null, bool $pad = false) + { + $this->fields = $fields ?? ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert']; + $this->pad = $pad; + } + + /** + * Transforms a normalized date interval into an interval array. + * + * @param \DateInterval $dateInterval Normalized date interval + * + * @throws UnexpectedTypeException if the given value is not a \DateInterval instance + */ + public function transform(mixed $dateInterval): array + { + if (null === $dateInterval) { + return array_intersect_key( + [ + 'years' => '', + 'months' => '', + 'weeks' => '', + 'days' => '', + 'hours' => '', + 'minutes' => '', + 'seconds' => '', + 'invert' => false, + ], + array_flip($this->fields) + ); + } + if (!$dateInterval instanceof \DateInterval) { + throw new UnexpectedTypeException($dateInterval, \DateInterval::class); + } + $result = []; + foreach (self::AVAILABLE_FIELDS as $field => $char) { + $result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char)); + } + if (\in_array('weeks', $this->fields, true)) { + $result['weeks'] = '0'; + if (isset($result['days']) && (int) $result['days'] >= 7) { + $result['weeks'] = (string) floor($result['days'] / 7); + $result['days'] = (string) ($result['days'] % 7); + } + } + $result['invert'] = '-' === $result['invert']; + $result = array_intersect_key($result, array_flip($this->fields)); + + return $result; + } + + /** + * Transforms an interval array into a normalized date interval. + * + * @param array $value Interval array + * + * @throws UnexpectedTypeException if the given value is not an array + * @throws TransformationFailedException if the value could not be transformed + */ + public function reverseTransform(mixed $value): ?\DateInterval + { + if (null === $value) { + return null; + } + if (!\is_array($value)) { + throw new UnexpectedTypeException($value, 'array'); + } + if ('' === implode('', $value)) { + return null; + } + $emptyFields = []; + foreach ($this->fields as $field) { + if (!isset($value[$field])) { + $emptyFields[] = $field; + } + } + if (\count($emptyFields) > 0) { + throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields))); + } + if (isset($value['invert']) && !\is_bool($value['invert'])) { + throw new TransformationFailedException('The value of "invert" must be boolean.'); + } + foreach (self::AVAILABLE_FIELDS as $field => $char) { + if ('invert' !== $field && isset($value[$field]) && !ctype_digit((string) $value[$field])) { + throw new TransformationFailedException(\sprintf('This amount of "%s" is invalid.', $field)); + } + } + try { + if (!empty($value['weeks'])) { + $interval = \sprintf( + 'P%sY%sM%sWT%sH%sM%sS', + empty($value['years']) ? '0' : $value['years'], + empty($value['months']) ? '0' : $value['months'], + empty($value['weeks']) ? '0' : $value['weeks'], + empty($value['hours']) ? '0' : $value['hours'], + empty($value['minutes']) ? '0' : $value['minutes'], + empty($value['seconds']) ? '0' : $value['seconds'] + ); + } else { + $interval = \sprintf( + 'P%sY%sM%sDT%sH%sM%sS', + empty($value['years']) ? '0' : $value['years'], + empty($value['months']) ? '0' : $value['months'], + empty($value['days']) ? '0' : $value['days'], + empty($value['hours']) ? '0' : $value['hours'], + empty($value['minutes']) ? '0' : $value['minutes'], + empty($value['seconds']) ? '0' : $value['seconds'] + ); + } + $dateInterval = new \DateInterval($interval); + if (isset($value['invert'])) { + $dateInterval->invert = $value['invert'] ? 1 : 0; + } + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + return $dateInterval; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php new file mode 100644 index 0000000000..2914fbb32f --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Transforms between a date string and a DateInterval object. + * + * @author Steffen Roßkamp + * + * @implements DataTransformerInterface<\DateInterval, string> + */ +class DateIntervalToStringTransformer implements DataTransformerInterface +{ + private string $format; + + /** + * Transforms a \DateInterval instance to a string. + * + * @see \DateInterval::format() for supported formats + * + * @param string $format The date format + */ + public function __construct(string $format = 'P%yY%mM%dDT%hH%iM%sS') + { + $this->format = $format; + } + + /** + * Transforms a DateInterval object into a date string with the configured format. + * + * @param \DateInterval|null $value A DateInterval object + * + * @throws UnexpectedTypeException if the given value is not a \DateInterval instance + */ + public function transform(mixed $value): string + { + if (null === $value) { + return ''; + } + if (!$value instanceof \DateInterval) { + throw new UnexpectedTypeException($value, \DateInterval::class); + } + + return $value->format($this->format); + } + + /** + * Transforms a date string in the configured format into a DateInterval object. + * + * @param string $value An ISO 8601 or date string like date interval presentation + * + * @throws UnexpectedTypeException if the given value is not a string + * @throws TransformationFailedException if the date interval could not be parsed + */ + public function reverseTransform(mixed $value): ?\DateInterval + { + if (null === $value) { + return null; + } + if (!\is_string($value)) { + throw new UnexpectedTypeException($value, 'string'); + } + if ('' === $value) { + return null; + } + if (!$this->isISO8601($value)) { + throw new TransformationFailedException('Non ISO 8601 date strings are not supported yet.'); + } + $valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/'; + if (!preg_match($valuePattern, $value)) { + throw new TransformationFailedException(\sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format)); + } + try { + $dateInterval = new \DateInterval($value); + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + return $dateInterval; + } + + private function isISO8601(string $string): bool + { + return preg_match('/^P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string); + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php new file mode 100644 index 0000000000..3f285b4a3d --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.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\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a DateTimeImmutable object and a DateTime object. + * + * @author Valentin Udaltsov + * + * @implements DataTransformerInterface<\DateTimeImmutable, \DateTime> + */ +final class DateTimeImmutableToDateTimeTransformer implements DataTransformerInterface +{ + /** + * Transforms a DateTimeImmutable into a DateTime object. + * + * @param \DateTimeImmutable|null $value A DateTimeImmutable object + * + * @throws TransformationFailedException If the given value is not a \DateTimeImmutable + */ + public function transform(mixed $value): ?\DateTime + { + if (null === $value) { + return null; + } + + if (!$value instanceof \DateTimeImmutable) { + throw new TransformationFailedException('Expected a \DateTimeImmutable.'); + } + + return \DateTime::createFromImmutable($value); + } + + /** + * Transforms a DateTime object into a DateTimeImmutable object. + * + * @param \DateTime|null $value A DateTime object + * + * @throws TransformationFailedException If the given value is not a \DateTime + */ + public function reverseTransform(mixed $value): ?\DateTimeImmutable + { + if (null === $value) { + return null; + } + + if (!$value instanceof \DateTime) { + throw new TransformationFailedException('Expected a \DateTime.'); + } + + return \DateTimeImmutable::createFromMutable($value); + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php new file mode 100644 index 0000000000..c7b858cd66 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a normalized time and a localized time string/array. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer + */ +class DateTimeToArrayTransformer extends BaseDateTimeTransformer +{ + private bool $pad; + private array $fields; + private \DateTimeInterface $referenceDate; + + /** + * @param string|null $inputTimezone The input timezone + * @param string|null $outputTimezone The output timezone + * @param string[]|null $fields The date fields + * @param bool $pad Whether to use padding + */ + public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, ?array $fields = null, bool $pad = false, ?\DateTimeInterface $referenceDate = null) + { + parent::__construct($inputTimezone, $outputTimezone); + + $this->fields = $fields ?? ['year', 'month', 'day', 'hour', 'minute', 'second']; + $this->pad = $pad; + $this->referenceDate = $referenceDate ?? new \DateTimeImmutable('1970-01-01 00:00:00'); + } + + /** + * Transforms a normalized date into a localized date. + * + * @param \DateTimeInterface $dateTime A DateTimeInterface object + * + * @throws TransformationFailedException If the given value is not a \DateTimeInterface + */ + public function transform(mixed $dateTime): array + { + if (null === $dateTime) { + return array_intersect_key([ + 'year' => '', + 'month' => '', + 'day' => '', + 'hour' => '', + 'minute' => '', + 'second' => '', + ], array_flip($this->fields)); + } + + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); + } + + if ($this->inputTimezone !== $this->outputTimezone) { + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); + $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); + } + + $result = array_intersect_key([ + 'year' => $dateTime->format('Y'), + 'month' => $dateTime->format('m'), + 'day' => $dateTime->format('d'), + 'hour' => $dateTime->format('H'), + 'minute' => $dateTime->format('i'), + 'second' => $dateTime->format('s'), + ], array_flip($this->fields)); + + if (!$this->pad) { + foreach ($result as &$entry) { + // remove leading zeros + $entry = (string) (int) $entry; + } + // unset reference to keep scope clear + unset($entry); + } + + return $result; + } + + /** + * Transforms a localized date into a normalized date. + * + * @param array $value Localized date + * + * @throws TransformationFailedException If the given value is not an array, + * if the value could not be transformed + */ + public function reverseTransform(mixed $value): ?\DateTime + { + if (null === $value) { + return null; + } + + if (!\is_array($value)) { + throw new TransformationFailedException('Expected an array.'); + } + + if ('' === implode('', $value)) { + return null; + } + + $emptyFields = []; + + foreach ($this->fields as $field) { + if (!isset($value[$field])) { + $emptyFields[] = $field; + } + } + + if (\count($emptyFields) > 0) { + throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields))); + } + + if (isset($value['month']) && !ctype_digit((string) $value['month'])) { + throw new TransformationFailedException('This month is invalid.'); + } + + if (isset($value['day']) && !ctype_digit((string) $value['day'])) { + throw new TransformationFailedException('This day is invalid.'); + } + + if (isset($value['year']) && !ctype_digit((string) $value['year'])) { + throw new TransformationFailedException('This year is invalid.'); + } + + if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) { + throw new TransformationFailedException('This is an invalid date.'); + } + + if (isset($value['hour']) && !ctype_digit((string) $value['hour'])) { + throw new TransformationFailedException('This hour is invalid.'); + } + + if (isset($value['minute']) && !ctype_digit((string) $value['minute'])) { + throw new TransformationFailedException('This minute is invalid.'); + } + + if (isset($value['second']) && !ctype_digit((string) $value['second'])) { + throw new TransformationFailedException('This second is invalid.'); + } + + try { + $dateTime = new \DateTime(\sprintf( + '%s-%s-%s %s:%s:%s', + empty($value['year']) ? $this->referenceDate->format('Y') : $value['year'], + empty($value['month']) ? $this->referenceDate->format('m') : $value['month'], + empty($value['day']) ? $this->referenceDate->format('d') : $value['day'], + $value['hour'] ?? $this->referenceDate->format('H'), + $value['minute'] ?? $this->referenceDate->format('i'), + $value['second'] ?? $this->referenceDate->format('s') + ), + new \DateTimeZone($this->outputTimezone) + ); + + if ($this->inputTimezone !== $this->outputTimezone) { + $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); + } + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + return $dateTime; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php new file mode 100644 index 0000000000..dbeed8e490 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @author Franz Wilding + * @author Bernhard Schussek + * @author Fred Cox + * + * @extends BaseDateTimeTransformer + */ +class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer +{ + public const HTML5_FORMAT = 'Y-m-d\\TH:i:s'; + public const HTML5_FORMAT_NO_SECONDS = 'Y-m-d\\TH:i'; + + public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, private bool $withSeconds = false) + { + parent::__construct($inputTimezone, $outputTimezone); + } + + /** + * Transforms a \DateTime into a local date and time string. + * + * According to the HTML standard, the input string of a datetime-local + * input is an RFC3339 date followed by 'T', followed by an RFC3339 time. + * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string + * + * @param \DateTimeInterface $dateTime + * + * @throws TransformationFailedException If the given value is not an + * instance of \DateTime or \DateTimeInterface + */ + public function transform(mixed $dateTime): string + { + if (null === $dateTime) { + return ''; + } + + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); + } + + if ($this->inputTimezone !== $this->outputTimezone) { + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); + $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); + } + + return $dateTime->format($this->withSeconds ? self::HTML5_FORMAT : self::HTML5_FORMAT_NO_SECONDS); + } + + /** + * Transforms a local date and time string into a \DateTime. + * + * When transforming back to DateTime the regex is slightly laxer, taking into + * account rules for parsing a local date and time string + * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string + * + * @param string $dateTimeLocal Formatted string + * + * @throws TransformationFailedException If the given value is not a string, + * if the value could not be transformed + */ + public function reverseTransform(mixed $dateTimeLocal): ?\DateTime + { + if (!\is_string($dateTimeLocal)) { + throw new TransformationFailedException('Expected a string.'); + } + + if ('' === $dateTimeLocal) { + return null; + } + + // to maintain backwards compatibility we do not strictly validate the submitted date + // see https://github.com/symfony/symfony/issues/28699 + if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/', $dateTimeLocal, $matches)) { + throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $dateTimeLocal)); + } + + try { + $dateTime = new \DateTime($dateTimeLocal, new \DateTimeZone($this->outputTimezone)); + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) { + $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); + } + + if (!checkdate($matches[2], $matches[3], $matches[1])) { + throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3])); + } + + return $dateTime; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php new file mode 100644 index 0000000000..5a07147912 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Transforms between a normalized time and a localized time string. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer + */ +class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer +{ + private int $dateFormat; + private int $timeFormat; + private ?string $pattern; + private int $calendar; + + /** + * @see BaseDateTimeTransformer::formats for available format options + * + * @param string|null $inputTimezone The name of the input timezone + * @param string|null $outputTimezone The name of the output timezone + * @param int|null $dateFormat The date format + * @param int|null $timeFormat The time format + * @param int $calendar One of the \IntlDateFormatter calendar constants + * @param string|null $pattern A pattern to pass to \IntlDateFormatter + * + * @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string + */ + public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, ?int $dateFormat = null, ?int $timeFormat = null, int $calendar = \IntlDateFormatter::GREGORIAN, ?string $pattern = null) + { + parent::__construct($inputTimezone, $outputTimezone); + + $dateFormat ??= \IntlDateFormatter::MEDIUM; + $timeFormat ??= \IntlDateFormatter::SHORT; + + if (!\in_array($dateFormat, self::$formats, true)) { + throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats)); + } + + if (!\in_array($timeFormat, self::$formats, true)) { + throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats)); + } + + $this->dateFormat = $dateFormat; + $this->timeFormat = $timeFormat; + $this->calendar = $calendar; + $this->pattern = $pattern; + } + + /** + * Transforms a normalized date into a localized date string/array. + * + * @param \DateTimeInterface $dateTime A DateTimeInterface object + * + * @throws TransformationFailedException if the given value is not a \DateTimeInterface + * or if the date could not be transformed + */ + public function transform(mixed $dateTime): string + { + if (null === $dateTime) { + return ''; + } + + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); + } + + $value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp()); + + if (0 != intl_get_error_code()) { + throw new TransformationFailedException(intl_get_error_message()); + } + + return $value; + } + + /** + * Transforms a localized date string/array into a normalized date. + * + * @param string $value Localized date string + * + * @throws TransformationFailedException if the given value is not a string, + * if the date could not be parsed + */ + public function reverseTransform(mixed $value): ?\DateTime + { + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + if ('' === $value) { + return null; + } + + // date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due + // to DST changes + $dateOnly = $this->isPatternDateOnly(); + $dateFormatter = $this->getIntlDateFormatter($dateOnly); + + try { + $timestamp = @$dateFormatter->parse($value); + } catch (\IntlException $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + if (0 != intl_get_error_code()) { + throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code()); + } elseif ($timestamp > 253402214400) { + // This timestamp represents UTC midnight of 9999-12-31 to prevent 5+ digit years + throw new TransformationFailedException('Years beyond 9999 are not supported.'); + } elseif (false === $timestamp) { + // the value couldn't be parsed but the Intl extension didn't report an error code, this + // could be the case when the Intl polyfill is used which always returns 0 as the error code + throw new TransformationFailedException(\sprintf('"%s" could not be parsed as a date.', $value)); + } + + try { + if ($dateOnly) { + // we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight + $dateTime = new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone)); + } else { + // read timestamp into DateTime object - the formatter delivers a timestamp + $dateTime = new \DateTime(\sprintf('@%s', $timestamp)); + } + // set timezone separately, as it would be ignored if set via the constructor, + // see https://php.net/datetime.construct + $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + if ($this->outputTimezone !== $this->inputTimezone) { + $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); + } + + return $dateTime; + } + + /** + * Returns a preconfigured IntlDateFormatter instance. + * + * @param bool $ignoreTimezone Use UTC regardless of the configured timezone + * + * @throws TransformationFailedException in case the date formatter cannot be constructed + */ + protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDateFormatter + { + $dateFormat = $this->dateFormat; + $timeFormat = $this->timeFormat; + $timezone = new \DateTimeZone($ignoreTimezone ? 'UTC' : $this->outputTimezone); + + $calendar = $this->calendar; + $pattern = $this->pattern; + + $intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern ?? ''); + + // new \intlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323 + if (!$intlDateFormatter) { + throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code()); + } + + $intlDateFormatter->setLenient(false); + + return $intlDateFormatter; + } + + /** + * Checks if the pattern contains only a date. + */ + protected function isPatternDateOnly(): bool + { + if (null === $this->pattern) { + return false; + } + + // strip escaped text + $pattern = preg_replace("#'(.*?)'#", '', $this->pattern); + + // check for the absence of time-related placeholders + return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern); + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php new file mode 100644 index 0000000000..d32b3eae2b --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @author Bernhard Schussek + * + * @extends BaseDateTimeTransformer + */ +class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer +{ + /** + * Transforms a normalized date into a localized date. + * + * @param \DateTimeInterface $dateTime A DateTimeInterface object + * + * @throws TransformationFailedException If the given value is not a \DateTimeInterface + */ + public function transform(mixed $dateTime): string + { + if (null === $dateTime) { + return ''; + } + + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); + } + + if ($this->inputTimezone !== $this->outputTimezone) { + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); + $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); + } + + return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c')); + } + + /** + * Transforms a formatted string following RFC 3339 into a normalized date. + * + * @param string $rfc3339 Formatted string + * + * @throws TransformationFailedException If the given value is not a string, + * if the value could not be transformed + */ + public function reverseTransform(mixed $rfc3339): ?\DateTime + { + if (!\is_string($rfc3339)) { + throw new TransformationFailedException('Expected a string.'); + } + + if ('' === $rfc3339) { + return null; + } + + if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))$/', $rfc3339, $matches)) { + throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $rfc3339)); + } + + try { + $dateTime = new \DateTime($rfc3339); + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) { + $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); + } + + if (!checkdate($matches[2], $matches[3], $matches[1])) { + throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3])); + } + + return $dateTime; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php new file mode 100644 index 0000000000..96bdc7c0de --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a date string and a DateTime object. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer + */ +class DateTimeToStringTransformer extends BaseDateTimeTransformer +{ + /** + * Format used for generating strings. + */ + private string $generateFormat; + + /** + * Format used for parsing strings. + * + * Different than the {@link $generateFormat} because formats for parsing + * support additional characters in PHP that are not supported for + * generating strings. + */ + private string $parseFormat; + + /** + * Transforms a \DateTime instance to a string. + * + * @see \DateTime::format() for supported formats + * + * @param string|null $inputTimezone The name of the input timezone + * @param string|null $outputTimezone The name of the output timezone + * @param string $format The date format + * @param string|null $parseFormat The parse format when different from $format + */ + public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, string $format = 'Y-m-d H:i:s', ?string $parseFormat = null) + { + parent::__construct($inputTimezone, $outputTimezone); + + $this->generateFormat = $format; + $this->parseFormat = $parseFormat ?? $format; + + // See https://php.net/datetime.createfromformat + // The character "|" in the format makes sure that the parts of a date + // that are *not* specified in the format are reset to the corresponding + // values from 1970-01-01 00:00:00 instead of the current time. + // Without "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 12:32:47", + // where the time corresponds to the current server time. + // With "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 00:00:00", + // which is at least deterministic and thus used here. + if (!str_contains($this->parseFormat, '|')) { + $this->parseFormat .= '|'; + } + } + + /** + * Transforms a DateTime object into a date string with the configured format + * and timezone. + * + * @param \DateTimeInterface $dateTime A DateTimeInterface object + * + * @throws TransformationFailedException If the given value is not a \DateTimeInterface + */ + public function transform(mixed $dateTime): string + { + if (null === $dateTime) { + return ''; + } + + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); + } + + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); + $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); + + return $dateTime->format($this->generateFormat); + } + + /** + * Transforms a date string in the configured timezone into a DateTime object. + * + * @param string $value A value as produced by PHP's date() function + * + * @throws TransformationFailedException If the given value is not a string, + * or could not be transformed + */ + public function reverseTransform(mixed $value): ?\DateTime + { + if (empty($value)) { + return null; + } + + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + if (str_contains($value, "\0")) { + throw new TransformationFailedException('Null bytes not allowed'); + } + + $outputTz = new \DateTimeZone($this->outputTimezone); + $dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz); + + $lastErrors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0]; + + if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) { + throw new TransformationFailedException(implode(', ', array_merge(array_values($lastErrors['warnings']), array_values($lastErrors['errors'])))); + } + + try { + if ($this->inputTimezone !== $this->outputTimezone) { + $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); + } + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + return $dateTime; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php new file mode 100644 index 0000000000..33c1b1d599 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a timestamp and a DateTime object. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer + */ +class DateTimeToTimestampTransformer extends BaseDateTimeTransformer +{ + /** + * Transforms a DateTime object into a timestamp in the configured timezone. + * + * @param \DateTimeInterface $dateTime A DateTimeInterface object + * + * @throws TransformationFailedException If the given value is not a \DateTimeInterface + */ + public function transform(mixed $dateTime): ?int + { + if (null === $dateTime) { + return null; + } + + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); + } + + return $dateTime->getTimestamp(); + } + + /** + * Transforms a timestamp in the configured timezone into a DateTime object. + * + * @param string $value A timestamp + * + * @throws TransformationFailedException If the given value is not a timestamp + * or if the given timestamp is invalid + */ + public function reverseTransform(mixed $value): ?\DateTime + { + if (null === $value) { + return null; + } + + if (!is_numeric($value)) { + throw new TransformationFailedException('Expected a numeric.'); + } + + try { + $dateTime = new \DateTime(); + $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); + $dateTime->setTimestamp($value); + + if ($this->inputTimezone !== $this->outputTimezone) { + $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); + } + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + return $dateTime; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php new file mode 100644 index 0000000000..f7bda17511 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a timezone identifier string and a DateTimeZone object. + * + * @author Roland Franssen + * + * @implements DataTransformerInterface<\DateTimeZone|array<\DateTimeZone>, string|array> + */ +class DateTimeZoneToStringTransformer implements DataTransformerInterface +{ + private bool $multiple; + + public function __construct(bool $multiple = false) + { + $this->multiple = $multiple; + } + + public function transform(mixed $dateTimeZone): mixed + { + if (null === $dateTimeZone) { + return null; + } + + if ($this->multiple) { + if (!\is_array($dateTimeZone)) { + throw new TransformationFailedException('Expected an array of \DateTimeZone objects.'); + } + + return array_map([new self(), 'transform'], $dateTimeZone); + } + + if (!$dateTimeZone instanceof \DateTimeZone) { + throw new TransformationFailedException('Expected a \DateTimeZone object.'); + } + + return $dateTimeZone->getName(); + } + + public function reverseTransform(mixed $value): mixed + { + if (null === $value) { + return null; + } + + if ($this->multiple) { + if (!\is_array($value)) { + throw new TransformationFailedException('Expected an array of timezone identifier strings.'); + } + + return array_map([new self(), 'reverseTransform'], $value); + } + + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a timezone identifier string.'); + } + + try { + return new \DateTimeZone($value); + } catch (\Exception $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php new file mode 100644 index 0000000000..d83e31b42d --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between an integer and a localized number with grouping + * (each thousand) and comma separators. + * + * @author Bernhard Schussek + */ +class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer +{ + /** + * Constructs a transformer. + * + * @param bool $grouping Whether thousands should be grouped + * @param int|null $roundingMode One of the ROUND_ constants in this class + * @param string|null $locale locale used for transforming + */ + public function __construct(?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_DOWN, ?string $locale = null) + { + parent::__construct(0, $grouping, $roundingMode, $locale); + } + + public function reverseTransform(mixed $value): int|float|null + { + $decimalSeparator = $this->getNumberFormatter()->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + + if (\is_string($value) && str_contains($value, $decimalSeparator)) { + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid integer.', $value)); + } + + $result = parent::reverseTransform($value); + + return null !== $result ? (int) $result : null; + } + + /** + * @internal + */ + protected function castParsedValue(int|float $value): int|float + { + return $value; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php new file mode 100644 index 0000000000..4e6533ec89 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a timezone identifier string and a IntlTimeZone object. + * + * @author Roland Franssen + * + * @implements DataTransformerInterface<\IntlTimeZone|array<\IntlTimeZone>, string|array> + */ +class IntlTimeZoneToStringTransformer implements DataTransformerInterface +{ + private bool $multiple; + + public function __construct(bool $multiple = false) + { + $this->multiple = $multiple; + } + + public function transform(mixed $intlTimeZone): mixed + { + if (null === $intlTimeZone) { + return null; + } + + if ($this->multiple) { + if (!\is_array($intlTimeZone)) { + throw new TransformationFailedException('Expected an array of \IntlTimeZone objects.'); + } + + return array_map([new self(), 'transform'], $intlTimeZone); + } + + if (!$intlTimeZone instanceof \IntlTimeZone) { + throw new TransformationFailedException('Expected a \IntlTimeZone object.'); + } + + return $intlTimeZone->getID(); + } + + public function reverseTransform(mixed $value): mixed + { + if (null === $value) { + return null; + } + + if ($this->multiple) { + if (!\is_array($value)) { + throw new TransformationFailedException('Expected an array of timezone identifier strings.'); + } + + return array_map([new self(), 'reverseTransform'], $value); + } + + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a timezone identifier string.'); + } + + $intlTimeZone = \IntlTimeZone::createTimeZone($value); + + if ('Etc/Unknown' === $intlTimeZone->getID()) { + throw new TransformationFailedException(\sprintf('Unknown timezone identifier "%s".', $value)); + } + + return $intlTimeZone; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php new file mode 100644 index 0000000000..d862b885d8 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a normalized format and a localized money string. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + */ +class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer +{ + private int $divisor; + + public function __construct(?int $scale = 2, ?bool $grouping = true, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?int $divisor = 1, ?string $locale = null) + { + parent::__construct($scale ?? 2, $grouping ?? true, $roundingMode, $locale); + + $this->divisor = $divisor ?? 1; + } + + /** + * Transforms a normalized format into a localized money string. + * + * @param int|float|string|null $value Normalized number + * + * @throws TransformationFailedException if the given value is not numeric or + * if the value cannot be transformed + */ + public function transform(mixed $value): string + { + if (null !== $value && '' !== $value && 1 !== $this->divisor) { + if (!is_numeric($value)) { + throw new TransformationFailedException('Expected a numeric.'); + } + $value /= $this->divisor; + } + + return parent::transform($value); + } + + /** + * Transforms a localized money string into a normalized format. + * + * @param string $value Localized money string + * + * @throws TransformationFailedException if the given value is not a string + * or if the value cannot be transformed + */ + public function reverseTransform(mixed $value): int|float|null + { + $value = parent::reverseTransform($value); + if (null !== $value && 1 !== $this->divisor) { + $value = (float) (string) ($value * $this->divisor); + } + + return $value; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php new file mode 100644 index 0000000000..6e9db3e091 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -0,0 +1,231 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between a number type and a localized number with grouping + * (each thousand) and comma separators. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @implements DataTransformerInterface + */ +class NumberToLocalizedStringTransformer implements DataTransformerInterface +{ + protected $grouping; + + protected $roundingMode; + + private ?int $scale; + private ?string $locale; + + public function __construct(?int $scale = null, ?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?string $locale = null) + { + $this->scale = $scale; + $this->grouping = $grouping ?? false; + $this->roundingMode = $roundingMode ?? \NumberFormatter::ROUND_HALFUP; + $this->locale = $locale; + } + + /** + * Transforms a number type into localized number. + * + * @param int|float|string|null $value Number value + * + * @throws TransformationFailedException if the given value is not numeric + * or if the value cannot be transformed + */ + public function transform(mixed $value): string + { + if (null === $value || '' === $value) { + return ''; + } + + if (!is_numeric($value)) { + throw new TransformationFailedException('Expected a numeric.'); + } + + $formatter = $this->getNumberFormatter(); + $value = $formatter->format($value); + + if (intl_is_failure($formatter->getErrorCode())) { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + // Convert non-breaking and narrow non-breaking spaces to normal ones + $value = str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value); + + return $value; + } + + /** + * Transforms a localized number into an integer or float. + * + * @param string $value The localized value + * + * @throws TransformationFailedException if the given value is not a string + * or if the value cannot be transformed + */ + public function reverseTransform(mixed $value): int|float|null + { + if (null !== $value && !\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + if (null === $value || '' === $value) { + return null; + } + + if (\in_array($value, ['NaN', 'NAN', 'nan'], true)) { + throw new TransformationFailedException('"NaN" is not a valid number.'); + } + + $position = 0; + $formatter = $this->getNumberFormatter(); + $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL); + $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + + if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) { + $value = str_replace('.', $decSep, $value); + } + + if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) { + $value = str_replace(',', $decSep, $value); + } + + // If the value is in exponential notation with a negative exponent, we end up with a float value too + if (str_contains($value, $decSep) || false !== stripos($value, 'e-')) { + $type = \NumberFormatter::TYPE_DOUBLE; + } else { + $type = \PHP_INT_SIZE === 8 + ? \NumberFormatter::TYPE_INT64 + : \NumberFormatter::TYPE_INT32; + } + + try { + $result = @$formatter->parse($value, $type, $position); + } catch (\IntlException $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } + + if (intl_is_failure($formatter->getErrorCode())) { + throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode()); + } + + if ($result >= \PHP_INT_MAX || $result <= -\PHP_INT_MAX) { + throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like.'); + } + + $result = $this->castParsedValue($result); + + if (false !== $encoding = mb_detect_encoding($value, null, true)) { + $length = mb_strlen($value, $encoding); + $remainder = mb_substr($value, $position, $length, $encoding); + } else { + $length = \strlen($value); + $remainder = substr($value, $position, $length); + } + + // After parsing, position holds the index of the character where the + // parsing stopped + if ($position < $length) { + // Check if there are unrecognized characters at the end of the + // number (excluding whitespace characters) + $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0"); + + if ('' !== $remainder) { + throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder)); + } + } + + // NumberFormatter::parse() does not round + return $this->round($result); + } + + /** + * Returns a preconfigured \NumberFormatter instance. + */ + protected function getNumberFormatter(): \NumberFormatter + { + $formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL); + + if (null !== $this->scale) { + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale); + $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); + } + + $formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping); + + return $formatter; + } + + /** + * @internal + */ + protected function castParsedValue(int|float $value): int|float + { + if (\is_int($value) && $value === (int) $float = (float) $value) { + return $float; + } + + return $value; + } + + /** + * Rounds a number according to the configured scale and rounding mode. + */ + private function round(int|float $number): int|float + { + if (\is_int($number)) { + return $number; + } + + if (null !== $this->scale && null !== $this->roundingMode) { + // shift number to maintain the correct scale during rounding + $roundingCoef = 10 ** $this->scale; + // string representation to avoid rounding errors, similar to bcmul() + $number = (string) ($number * $roundingCoef); + + switch ($this->roundingMode) { + case \NumberFormatter::ROUND_CEILING: + $number = ceil($number); + break; + case \NumberFormatter::ROUND_FLOOR: + $number = floor($number); + break; + case \NumberFormatter::ROUND_UP: + $number = $number > 0 ? ceil($number) : floor($number); + break; + case \NumberFormatter::ROUND_DOWN: + $number = $number > 0 ? floor($number) : ceil($number); + break; + case \NumberFormatter::ROUND_HALFEVEN: + $number = round($number, 0, \PHP_ROUND_HALF_EVEN); + break; + case \NumberFormatter::ROUND_HALFUP: + $number = round($number, 0, \PHP_ROUND_HALF_UP); + break; + case \NumberFormatter::ROUND_HALFDOWN: + $number = round($number, 0, \PHP_ROUND_HALF_DOWN); + break; + } + + $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; + } + + return $number; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php new file mode 100644 index 0000000000..3f662adb71 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Transforms between a normalized format (integer or float) and a percentage value. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * + * @implements DataTransformerInterface + */ +class PercentToLocalizedStringTransformer implements DataTransformerInterface +{ + public const FRACTIONAL = 'fractional'; + public const INTEGER = 'integer'; + + protected static $types = [ + self::FRACTIONAL, + self::INTEGER, + ]; + + private int $roundingMode; + private string $type; + private int $scale; + private bool $html5Format; + + /** + * @see self::$types for a list of supported types + * + * @param int $roundingMode A value from \NumberFormatter, such as \NumberFormatter::ROUND_HALFUP + * @param bool $html5Format Use an HTML5 specific format, see https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats + * + * @throws UnexpectedTypeException if the given value of type is unknown + */ + public function __construct(?int $scale = null, ?string $type = null, int $roundingMode = \NumberFormatter::ROUND_HALFUP, bool $html5Format = false) + { + $type ??= self::FRACTIONAL; + + if (!\in_array($type, self::$types, true)) { + throw new UnexpectedTypeException($type, implode('", "', self::$types)); + } + + $this->type = $type; + $this->scale = $scale ?? 0; + $this->roundingMode = $roundingMode; + $this->html5Format = $html5Format; + } + + /** + * Transforms between a normalized format (integer or float) into a percentage value. + * + * @param int|float $value Normalized value + * + * @throws TransformationFailedException if the given value is not numeric or + * if the value could not be transformed + */ + public function transform(mixed $value): string + { + if (null === $value) { + return ''; + } + + if (!is_numeric($value)) { + throw new TransformationFailedException('Expected a numeric.'); + } + + if (self::FRACTIONAL == $this->type) { + $value *= 100; + } + + $formatter = $this->getNumberFormatter(); + $value = $formatter->format($value); + + if (intl_is_failure($formatter->getErrorCode())) { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + // replace the UTF-8 non break spaces + return $value; + } + + /** + * Transforms between a percentage value into a normalized format (integer or float). + * + * @param string $value Percentage value + * + * @throws TransformationFailedException if the given value is not a string or + * if the value could not be transformed + */ + public function reverseTransform(mixed $value): int|float|null + { + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + if ('' === $value) { + return null; + } + + $position = 0; + $formatter = $this->getNumberFormatter(); + $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL); + $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + $grouping = $formatter->getAttribute(\NumberFormatter::GROUPING_USED); + + if ('.' !== $decSep && (!$grouping || '.' !== $groupSep)) { + $value = str_replace('.', $decSep, $value); + } + + if (',' !== $decSep && (!$grouping || ',' !== $groupSep)) { + $value = str_replace(',', $decSep, $value); + } + + if (str_contains($value, $decSep)) { + $type = \NumberFormatter::TYPE_DOUBLE; + } else { + $type = \PHP_INT_SIZE === 8 ? \NumberFormatter::TYPE_INT64 : \NumberFormatter::TYPE_INT32; + } + + try { + // replace normal spaces so that the formatter can read them + $result = @$formatter->parse(str_replace(' ', "\xc2\xa0", $value), $type, $position); + } catch (\IntlException $e) { + throw new TransformationFailedException($e->getMessage(), 0, $e); + } + + if (intl_is_failure($formatter->getErrorCode())) { + throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode()); + } + + if (self::FRACTIONAL == $this->type) { + $result /= 100; + } + + if (\function_exists('mb_detect_encoding') && false !== $encoding = mb_detect_encoding($value, null, true)) { + $length = mb_strlen($value, $encoding); + $remainder = mb_substr($value, $position, $length, $encoding); + } else { + $length = \strlen($value); + $remainder = substr($value, $position, $length); + } + + // After parsing, position holds the index of the character where the + // parsing stopped + if ($position < $length) { + // Check if there are unrecognized characters at the end of the + // number (excluding whitespace characters) + $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0"); + + if ('' !== $remainder) { + throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder)); + } + } + + return $this->round($result); + } + + /** + * Returns a preconfigured \NumberFormatter instance. + */ + protected function getNumberFormatter(): \NumberFormatter + { + // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping, + // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats + $formatter = new \NumberFormatter($this->html5Format ? 'en' : \Locale::getDefault(), \NumberFormatter::DECIMAL); + + if ($this->html5Format) { + $formatter->setAttribute(\NumberFormatter::GROUPING_USED, 0); + } + + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale); + + if (null !== $this->roundingMode) { + $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); + } + + return $formatter; + } + + /** + * Rounds a number according to the configured scale and rounding mode. + */ + private function round(int|float $number): int|float + { + if (null !== $this->scale && null !== $this->roundingMode) { + // shift number to maintain the correct scale during rounding + $roundingCoef = 10 ** $this->scale; + + if (self::FRACTIONAL == $this->type) { + $roundingCoef *= 100; + } + + // string representation to avoid rounding errors, similar to bcmul() + $number = (string) ($number * $roundingCoef); + + switch ($this->roundingMode) { + case \NumberFormatter::ROUND_CEILING: + $number = ceil($number); + break; + case \NumberFormatter::ROUND_FLOOR: + $number = floor($number); + break; + case \NumberFormatter::ROUND_UP: + $number = $number > 0 ? ceil($number) : floor($number); + break; + case \NumberFormatter::ROUND_DOWN: + $number = $number > 0 ? floor($number) : ceil($number); + break; + case \NumberFormatter::ROUND_HALFEVEN: + $number = round($number, 0, \PHP_ROUND_HALF_EVEN); + break; + case \NumberFormatter::ROUND_HALFUP: + $number = round($number, 0, \PHP_ROUND_HALF_UP); + break; + case \NumberFormatter::ROUND_HALFDOWN: + $number = round($number, 0, \PHP_ROUND_HALF_DOWN); + break; + } + + $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; + } + + return $number; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/StringToFloatTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/StringToFloatTransformer.php new file mode 100644 index 0000000000..49b4ea98ab --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/StringToFloatTransformer.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\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @implements DataTransformerInterface + */ +class StringToFloatTransformer implements DataTransformerInterface +{ + private ?int $scale; + + public function __construct(?int $scale = null) + { + $this->scale = $scale; + } + + public function transform(mixed $value): ?float + { + if (null === $value) { + return null; + } + + if (!\is_string($value) || !is_numeric($value)) { + throw new TransformationFailedException('Expected a numeric string.'); + } + + return (float) $value; + } + + public function reverseTransform(mixed $value): ?string + { + if (null === $value) { + return null; + } + + if (!\is_int($value) && !\is_float($value)) { + throw new TransformationFailedException('Expected a numeric.'); + } + + if ($this->scale > 0) { + return number_format((float) $value, $this->scale, '.', ''); + } + + return (string) $value; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/UlidToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/UlidToStringTransformer.php new file mode 100644 index 0000000000..9365cab15e --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/UlidToStringTransformer.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Uid\Ulid; + +/** + * Transforms between a ULID string and a Ulid object. + * + * @author Pavel Dyakonov + * + * @implements DataTransformerInterface + */ +class UlidToStringTransformer implements DataTransformerInterface +{ + /** + * Transforms a Ulid object into a string. + * + * @param Ulid $value A Ulid object + * + * @throws TransformationFailedException If the given value is not a Ulid object + */ + public function transform(mixed $value): ?string + { + if (null === $value) { + return null; + } + + if (!$value instanceof Ulid) { + throw new TransformationFailedException('Expected a Ulid.'); + } + + return (string) $value; + } + + /** + * Transforms a ULID string into a Ulid object. + * + * @param string $value A ULID string + * + * @throws TransformationFailedException If the given value is not a string, + * or could not be transformed + */ + public function reverseTransform(mixed $value): ?Ulid + { + if (null === $value || '' === $value) { + return null; + } + + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + try { + $ulid = new Ulid($value); + } catch (\InvalidArgumentException $e) { + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid ULID.', $value), $e->getCode(), $e); + } + + return $ulid; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/UuidToStringTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/UuidToStringTransformer.php new file mode 100644 index 0000000000..43326eb644 --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/UuidToStringTransformer.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Uid\Uuid; + +/** + * Transforms between a UUID string and a Uuid object. + * + * @author Pavel Dyakonov + * + * @implements DataTransformerInterface + */ +class UuidToStringTransformer implements DataTransformerInterface +{ + /** + * Transforms a Uuid object into a string. + * + * @param Uuid $value A Uuid object + * + * @throws TransformationFailedException If the given value is not a Uuid object + */ + public function transform(mixed $value): ?string + { + if (null === $value) { + return null; + } + + if (!$value instanceof Uuid) { + throw new TransformationFailedException('Expected a Uuid.'); + } + + return (string) $value; + } + + /** + * Transforms a UUID string into a Uuid object. + * + * @param string $value A UUID string + * + * @throws TransformationFailedException If the given value is not a string, + * or could not be transformed + */ + public function reverseTransform(mixed $value): ?Uuid + { + if (null === $value || '' === $value) { + return null; + } + + if (!\is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + if (!Uuid::isValid($value)) { + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value)); + } + + try { + return Uuid::fromString($value); + } catch (\InvalidArgumentException $e) { + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e); + } + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php new file mode 100644 index 0000000000..c5d12157ae --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * @author Bernhard Schussek + * + * @implements DataTransformerInterface + */ +class ValueToDuplicatesTransformer implements DataTransformerInterface +{ + private array $keys; + + public function __construct(array $keys) + { + $this->keys = $keys; + } + + /** + * Duplicates the given value through the array. + */ + public function transform(mixed $value): array + { + $result = []; + + foreach ($this->keys as $key) { + $result[$key] = $value; + } + + return $result; + } + + /** + * Extracts the duplicated value from an array. + * + * @throws TransformationFailedException if the given value is not an array or + * if the given array cannot be transformed + */ + public function reverseTransform(mixed $array): mixed + { + if (!\is_array($array)) { + throw new TransformationFailedException('Expected an array.'); + } + + $result = current($array); + $emptyKeys = []; + + foreach ($this->keys as $key) { + if (isset($array[$key]) && false !== $array[$key] && [] !== $array[$key]) { + if ($array[$key] !== $result) { + throw new TransformationFailedException('All values in the array should be the same.'); + } + } else { + $emptyKeys[] = $key; + } + } + + if (\count($emptyKeys) > 0) { + if (\count($emptyKeys) == \count($this->keys)) { + // All keys empty + return null; + } + + throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys))); + } + + return $result; + } +} diff --git a/lib/symfony/form/Extension/Core/DataTransformer/WeekToArrayTransformer.php b/lib/symfony/form/Extension/Core/DataTransformer/WeekToArrayTransformer.php new file mode 100644 index 0000000000..448ae4278b --- /dev/null +++ b/lib/symfony/form/Extension/Core/DataTransformer/WeekToArrayTransformer.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transforms between an ISO 8601 week date string and an array. + * + * @author Damien Fayet + * + * @implements DataTransformerInterface + */ +class WeekToArrayTransformer implements DataTransformerInterface +{ + /** + * Transforms a string containing an ISO 8601 week date into an array. + * + * @param string|null $value A week date string + * + * @return array{year: int|null, week: int|null} + * + * @throws TransformationFailedException If the given value is not a string, + * or if the given value does not follow the right format + */ + public function transform(mixed $value): array + { + if (null === $value) { + return ['year' => null, 'week' => null]; + } + + if (!\is_string($value)) { + throw new TransformationFailedException(\sprintf('Value is expected to be a string but was "%s".', get_debug_type($value))); + } + + if (0 === preg_match('/^(?P\d{4})-W(?P\d{2})$/', $value, $matches)) { + throw new TransformationFailedException('Given data does not follow the date format "Y-\WW".'); + } + + return [ + 'year' => (int) $matches['year'], + 'week' => (int) $matches['week'], + ]; + } + + /** + * Transforms an array into a week date string. + * + * @param array{year: int|null, week: int|null} $value + * + * @return string|null A week date string following the format Y-\WW + * + * @throws TransformationFailedException If the given value cannot be merged in a valid week date string, + * or if the obtained week date does not exists + */ + public function reverseTransform(mixed $value): ?string + { + if (null === $value || [] === $value) { + return null; + } + + if (!\is_array($value)) { + throw new TransformationFailedException(\sprintf('Value is expected to be an array, but was "%s".', get_debug_type($value))); + } + + if (!\array_key_exists('year', $value)) { + throw new TransformationFailedException('Key "year" is missing.'); + } + + if (!\array_key_exists('week', $value)) { + throw new TransformationFailedException('Key "week" is missing.'); + } + + if ($additionalKeys = array_diff(array_keys($value), ['year', 'week'])) { + throw new TransformationFailedException(\sprintf('Expected only keys "year" and "week" to be present, but also got ["%s"].', implode('", "', $additionalKeys))); + } + + if (null === $value['year'] && null === $value['week']) { + return null; + } + + if (!\is_int($value['year'])) { + throw new TransformationFailedException(\sprintf('Year is expected to be an integer, but was "%s".', get_debug_type($value['year']))); + } + + if (!\is_int($value['week'])) { + throw new TransformationFailedException(\sprintf('Week is expected to be an integer, but was "%s".', get_debug_type($value['week']))); + } + + // The 28th December is always in the last week of the year + if (date('W', strtotime('28th December '.$value['year'])) < $value['week']) { + throw new TransformationFailedException(\sprintf('Week "%d" does not exist for year "%d".', $value['week'], $value['year'])); + } + + return \sprintf('%d-W%02d', $value['year'], $value['week']); + } +} diff --git a/lib/symfony/form/Extension/Core/EventListener/FixUrlProtocolListener.php b/lib/symfony/form/Extension/Core/EventListener/FixUrlProtocolListener.php new file mode 100644 index 0000000000..7189977549 --- /dev/null +++ b/lib/symfony/form/Extension/Core/EventListener/FixUrlProtocolListener.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Adds a protocol to a URL if it doesn't already have one. + * + * @author Bernhard Schussek + */ +class FixUrlProtocolListener implements EventSubscriberInterface +{ + private ?string $defaultProtocol; + + /** + * @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data + */ + public function __construct(?string $defaultProtocol = 'http') + { + $this->defaultProtocol = $defaultProtocol; + } + + /** + * @return void + */ + public function onSubmit(FormEvent $event) + { + $data = $event->getData(); + + if ($this->defaultProtocol && $data && \is_string($data) && !preg_match('~^(?:[/.]|[\w+.-]+://|[^:/?@#]++@)~', $data)) { + $event->setData($this->defaultProtocol.'://'.$data); + } + } + + public static function getSubscribedEvents(): array + { + return [FormEvents::SUBMIT => 'onSubmit']; + } +} diff --git a/lib/symfony/form/Extension/Core/EventListener/MergeCollectionListener.php b/lib/symfony/form/Extension/Core/EventListener/MergeCollectionListener.php new file mode 100644 index 0000000000..62cd0a42a7 --- /dev/null +++ b/lib/symfony/form/Extension/Core/EventListener/MergeCollectionListener.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * @author Bernhard Schussek + */ +class MergeCollectionListener implements EventSubscriberInterface +{ + private bool $allowAdd; + private bool $allowDelete; + + /** + * @param bool $allowAdd Whether values might be added to the collection + * @param bool $allowDelete Whether values might be removed from the collection + */ + public function __construct(bool $allowAdd = false, bool $allowDelete = false) + { + $this->allowAdd = $allowAdd; + $this->allowDelete = $allowDelete; + } + + public static function getSubscribedEvents(): array + { + return [ + FormEvents::SUBMIT => 'onSubmit', + ]; + } + + /** + * @return void + */ + public function onSubmit(FormEvent $event) + { + $dataToMergeInto = $event->getForm()->getNormData(); + $data = $event->getData() ?? []; + + if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + if (null !== $dataToMergeInto && !\is_array($dataToMergeInto) && !($dataToMergeInto instanceof \Traversable && $dataToMergeInto instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($dataToMergeInto, 'array or (\Traversable and \ArrayAccess)'); + } + + // If we are not allowed to change anything, return immediately + if ($data === $dataToMergeInto || (!$this->allowAdd && !$this->allowDelete)) { + $event->setData($dataToMergeInto); + + return; + } + + if (null === $dataToMergeInto) { + // No original data was set. Set it if allowed + if ($this->allowAdd) { + $dataToMergeInto = $data; + } + } else { + // Calculate delta + $itemsToAdd = \is_object($data) ? clone $data : $data; + $itemsToDelete = []; + + foreach ($dataToMergeInto as $beforeKey => $beforeItem) { + foreach ($data as $afterKey => $afterItem) { + if ($afterItem === $beforeItem) { + // Item found, next original item + unset($itemsToAdd[$afterKey]); + continue 2; + } + } + + // Item not found, remember for deletion + $itemsToDelete[] = $beforeKey; + } + + // Remove deleted items before adding to free keys that are to be + // replaced + if ($this->allowDelete) { + foreach ($itemsToDelete as $key) { + unset($dataToMergeInto[$key]); + } + } + + // Add remaining items + if ($this->allowAdd) { + foreach ($itemsToAdd as $key => $item) { + if (!isset($dataToMergeInto[$key])) { + $dataToMergeInto[$key] = $item; + } else { + $dataToMergeInto[] = $item; + } + } + } + } + + $event->setData($dataToMergeInto); + } +} diff --git a/lib/symfony/form/Extension/Core/EventListener/ResizeFormListener.php b/lib/symfony/form/Extension/Core/EventListener/ResizeFormListener.php new file mode 100644 index 0000000000..63b09266a7 --- /dev/null +++ b/lib/symfony/form/Extension/Core/EventListener/ResizeFormListener.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; + +/** + * Resize a collection form element based on the data sent from the client. + * + * @author Bernhard Schussek + */ +class ResizeFormListener implements EventSubscriberInterface +{ + protected $type; + protected $options; + protected $prototypeOptions; + protected $allowAdd; + protected $allowDelete; + + private \Closure|bool $deleteEmpty; + + public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, bool|callable $deleteEmpty = false, ?array $prototypeOptions = null) + { + $this->type = $type; + $this->allowAdd = $allowAdd; + $this->allowDelete = $allowDelete; + $this->options = $options; + $this->deleteEmpty = \is_bool($deleteEmpty) ? $deleteEmpty : $deleteEmpty(...); + $this->prototypeOptions = $prototypeOptions ?? $options; + } + + public static function getSubscribedEvents(): array + { + return [ + FormEvents::PRE_SET_DATA => 'preSetData', + FormEvents::PRE_SUBMIT => 'preSubmit', + // (MergeCollectionListener, MergeDoctrineCollectionListener) + FormEvents::SUBMIT => ['onSubmit', 50], + ]; + } + + /** + * @return void + */ + public function preSetData(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData() ?? []; + + if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + // First remove all rows + foreach ($form as $name => $child) { + $form->remove($name); + } + + // Then add all rows again in the correct order + foreach ($data as $name => $value) { + $form->add($name, $this->type, array_replace([ + 'property_path' => '['.$name.']', + ], $this->options)); + } + } + + /** + * @return void + */ + public function preSubmit(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (!\is_array($data)) { + $data = []; + } + + // Remove all empty rows + if ($this->allowDelete) { + foreach ($form as $name => $child) { + if (!isset($data[$name])) { + $form->remove($name); + } + } + } + + // Add all additional rows + if ($this->allowAdd) { + foreach ($data as $name => $value) { + if (!$form->has($name)) { + $form->add($name, $this->type, array_replace([ + 'property_path' => '['.$name.']', + ], $this->prototypeOptions)); + } + } + } + } + + /** + * @return void + */ + public function onSubmit(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData() ?? []; + + // At this point, $data is an array or an array-like object that already contains the + // new entries, which were added by the data mapper. The data mapper ignores existing + // entries, so we need to manually unset removed entries in the collection. + + if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + if ($this->deleteEmpty) { + $previousData = $form->getData(); + /** @var FormInterface $child */ + foreach ($form as $name => $child) { + if (!$child->isValid() || !$child->isSynchronized()) { + continue; + } + + $isNew = !isset($previousData[$name]); + $isEmpty = \is_callable($this->deleteEmpty) ? ($this->deleteEmpty)($child->getData()) : $child->isEmpty(); + + // $isNew can only be true if allowAdd is true, so we don't + // need to check allowAdd again + if ($isEmpty && ($isNew || $this->allowDelete)) { + unset($data[$name]); + $form->remove($name); + } + } + } + + // The data mapper only adds, but does not remove items, so do this + // here + if ($this->allowDelete) { + $toDelete = []; + + foreach ($data as $name => $child) { + if (!$form->has($name)) { + $toDelete[] = $name; + } + } + + foreach ($toDelete as $name) { + unset($data[$name]); + } + } + + $event->setData($data); + } +} diff --git a/lib/symfony/form/Extension/Core/EventListener/TransformationFailureListener.php b/lib/symfony/form/Extension/Core/EventListener/TransformationFailureListener.php new file mode 100644 index 0000000000..cb9a675bef --- /dev/null +++ b/lib/symfony/form/Extension/Core/EventListener/TransformationFailureListener.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Christian Flothmann + */ +class TransformationFailureListener implements EventSubscriberInterface +{ + private ?TranslatorInterface $translator; + + public function __construct(?TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + public static function getSubscribedEvents(): array + { + return [ + FormEvents::POST_SUBMIT => ['convertTransformationFailureToFormError', -1024], + ]; + } + + /** + * @return void + */ + public function convertTransformationFailureToFormError(FormEvent $event) + { + $form = $event->getForm(); + + if (null === $form->getTransformationFailure() || !$form->isValid()) { + return; + } + + foreach ($form as $child) { + if (!$child->isSynchronized()) { + return; + } + } + + $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : get_debug_type($form->getViewData()); + $messageTemplate = $form->getConfig()->getOption('invalid_message', 'The value {{ value }} is not valid.'); + $messageParameters = array_replace(['{{ value }}' => $clientDataAsString], $form->getConfig()->getOption('invalid_message_parameters', [])); + + if (null !== $this->translator) { + $message = $this->translator->trans($messageTemplate, $messageParameters); + } else { + $message = strtr($messageTemplate, $messageParameters); + } + + $form->addError(new FormError($message, $messageTemplate, $messageParameters, null, $form->getTransformationFailure())); + } +} diff --git a/lib/symfony/form/Extension/Core/EventListener/TrimListener.php b/lib/symfony/form/Extension/Core/EventListener/TrimListener.php new file mode 100644 index 0000000000..81a55f3cb0 --- /dev/null +++ b/lib/symfony/form/Extension/Core/EventListener/TrimListener.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Util\StringUtil; + +/** + * Trims string data. + * + * @author Bernhard Schussek + */ +class TrimListener implements EventSubscriberInterface +{ + /** + * @return void + */ + public function preSubmit(FormEvent $event) + { + $data = $event->getData(); + + if (!\is_string($data)) { + return; + } + + $event->setData(StringUtil::trim($data)); + } + + public static function getSubscribedEvents(): array + { + return [FormEvents::PRE_SUBMIT => 'preSubmit']; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/BaseType.php b/lib/symfony/form/Extension/Core/Type/BaseType.php new file mode 100644 index 0000000000..bb50889c89 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/BaseType.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractRendererEngine; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Encapsulates common logic of {@link FormType} and {@link ButtonType}. + * + * This type does not appear in the form's type inheritance chain and as such + * cannot be extended (via {@link \Symfony\Component\Form\FormExtensionInterface}) nor themed. + * + * @author Bernhard Schussek + */ +abstract class BaseType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->setDisabled($options['disabled']); + $builder->setAutoInitialize($options['auto_initialize']); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $name = $form->getName(); + $blockName = $options['block_name'] ?: $form->getName(); + $translationDomain = $options['translation_domain']; + $labelTranslationParameters = $options['label_translation_parameters']; + $attrTranslationParameters = $options['attr_translation_parameters']; + $labelFormat = $options['label_format']; + + if ($view->parent) { + if ('' !== ($parentFullName = $view->parent->vars['full_name'])) { + $id = \sprintf('%s_%s', $view->parent->vars['id'], $name); + $fullName = \sprintf('%s[%s]', $parentFullName, $name); + $uniqueBlockPrefix = \sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName); + } else { + $id = $name; + $fullName = $name; + $uniqueBlockPrefix = '_'.$blockName; + } + + $translationDomain ??= $view->parent->vars['translation_domain']; + + $labelTranslationParameters = array_merge($view->parent->vars['label_translation_parameters'], $labelTranslationParameters); + $attrTranslationParameters = array_merge($view->parent->vars['attr_translation_parameters'], $attrTranslationParameters); + + if (!$labelFormat) { + $labelFormat = $view->parent->vars['label_format']; + } + + $rootFormAttrOption = $form->getRoot()->getConfig()->getOption('form_attr'); + if ($options['form_attr'] || $rootFormAttrOption) { + $options['attr']['form'] = \is_string($rootFormAttrOption) ? $rootFormAttrOption : $form->getRoot()->getName(); + if (empty($options['attr']['form'])) { + throw new LogicException('"form_attr" option must be a string identifier on root form when it has no id.'); + } + } + } else { + $id = \is_string($options['form_attr']) ? $options['form_attr'] : $name; + $fullName = $name; + $uniqueBlockPrefix = '_'.$blockName; + + // Strip leading underscores and digits. These are allowed in + // form names, but not in HTML4 ID attributes. + // https://www.w3.org/TR/html401/struct/global#adef-id + $id = ltrim($id, '_0123456789'); + } + + $blockPrefixes = []; + for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) { + array_unshift($blockPrefixes, $type->getBlockPrefix()); + } + if (null !== $options['block_prefix']) { + $blockPrefixes[] = $options['block_prefix']; + } + $blockPrefixes[] = $uniqueBlockPrefix; + + $view->vars = array_replace($view->vars, [ + 'form' => $view, + 'id' => $id, + 'name' => $name, + 'full_name' => $fullName, + 'disabled' => $form->isDisabled(), + 'label' => $options['label'], + 'label_format' => $labelFormat, + 'label_html' => $options['label_html'], + 'multipart' => false, + 'attr' => $options['attr'], + 'block_prefixes' => $blockPrefixes, + 'unique_block_prefix' => $uniqueBlockPrefix, + 'row_attr' => $options['row_attr'], + 'translation_domain' => $translationDomain, + 'label_translation_parameters' => $labelTranslationParameters, + 'attr_translation_parameters' => $attrTranslationParameters, + 'priority' => $options['priority'], + // Using the block name here speeds up performance in collection + // forms, where each entry has the same full block name. + // Including the type is important too, because if rows of a + // collection form have different types (dynamically), they should + // be rendered differently. + // https://github.com/symfony/symfony/issues/5038 + AbstractRendererEngine::CACHE_KEY_VAR => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(), + ]); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'block_name' => null, + 'block_prefix' => null, + 'disabled' => false, + 'label' => null, + 'label_format' => null, + 'row_attr' => [], + 'label_html' => false, + 'label_translation_parameters' => [], + 'attr_translation_parameters' => [], + 'attr' => [], + 'translation_domain' => null, + 'auto_initialize' => true, + 'priority' => 0, + 'form_attr' => false, + ]); + + $resolver->setAllowedTypes('block_prefix', ['null', 'string']); + $resolver->setAllowedTypes('attr', 'array'); + $resolver->setAllowedTypes('row_attr', 'array'); + $resolver->setAllowedTypes('label_html', 'bool'); + $resolver->setAllowedTypes('priority', 'int'); + $resolver->setAllowedTypes('form_attr', ['bool', 'string']); + + $resolver->setInfo('priority', 'The form rendering priority (higher priorities will be rendered first)'); + } +} diff --git a/lib/symfony/form/Extension/Core/Type/BirthdayType.php b/lib/symfony/form/Extension/Core/Type/BirthdayType.php new file mode 100644 index 0000000000..fa60d016eb --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/BirthdayType.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class BirthdayType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'years' => range((int) date('Y') - 120, date('Y')), + 'invalid_message' => 'Please enter a valid birthdate.', + ]); + + $resolver->setAllowedTypes('years', 'array'); + } + + public function getParent(): ?string + { + return DateType::class; + } + + public function getBlockPrefix(): string + { + return 'birthday'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/ButtonType.php b/lib/symfony/form/Extension/Core/Type/ButtonType.php new file mode 100644 index 0000000000..d710546407 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/ButtonType.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\ButtonTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * A form button. + * + * @author Bernhard Schussek + */ +class ButtonType extends BaseType implements ButtonTypeInterface +{ + public function getParent(): ?string + { + return null; + } + + public function getBlockPrefix(): string + { + return 'button'; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefault('auto_initialize', false); + } +} diff --git a/lib/symfony/form/Extension/Core/Type/CheckboxType.php b/lib/symfony/form/Extension/Core/Type/CheckboxType.php new file mode 100644 index 0000000000..291ede93ef --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/CheckboxType.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class CheckboxType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + // Unlike in other types, where the data is NULL by default, it + // needs to be a Boolean here. setData(null) is not acceptable + // for checkboxes and radio buttons (unless a custom model + // transformer handles this case). + // We cannot solve this case via overriding the "data" option, because + // doing so also calls setDataLocked(true). + $builder->setData($options['data'] ?? false); + $builder->addViewTransformer(new BooleanToStringTransformer($options['value'], $options['false_values'])); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars = array_replace($view->vars, [ + 'value' => $options['value'], + 'checked' => null !== $form->getViewData(), + ]); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $emptyData = static fn (FormInterface $form, $viewData) => $viewData; + + $resolver->setDefaults([ + 'value' => '1', + 'empty_data' => $emptyData, + 'compound' => false, + 'false_values' => [null], + 'invalid_message' => 'The checkbox has an invalid value.', + 'is_empty_callback' => static fn ($modelData): bool => false === $modelData, + ]); + + $resolver->setAllowedTypes('false_values', 'array'); + } + + public function getBlockPrefix(): string + { + return 'checkbox'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/ChoiceType.php b/lib/symfony/form/Extension/Core/Type/ChoiceType.php new file mode 100644 index 0000000000..ed12093bf4 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/ChoiceType.php @@ -0,0 +1,478 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters; +use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue; +use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy; +use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Event\PreSubmitEvent; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper; +use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; +use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Contracts\Translation\TranslatorInterface; + +class ChoiceType extends AbstractType +{ + private ChoiceListFactoryInterface $choiceListFactory; + private ?TranslatorInterface $translator; + + public function __construct(?ChoiceListFactoryInterface $choiceListFactory = null, ?TranslatorInterface $translator = null) + { + $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator( + new PropertyAccessDecorator( + new DefaultChoiceListFactory() + ) + ); + $this->translator = $translator; + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $unknownValues = []; + $choiceList = $this->createChoiceList($options); + $builder->setAttribute('choice_list', $choiceList); + + if ($options['expanded']) { + $builder->setDataMapper($options['multiple'] ? new CheckboxListMapper() : new RadioListMapper()); + + // Initialize all choices before doing the index check below. + // This helps in cases where index checks are optimized for non + // initialized choice lists. For example, when using an SQL driver, + // the index check would read in one SQL query and the initialization + // requires another SQL query. When the initialization is done first, + // one SQL query is sufficient. + + $choiceListView = $this->createChoiceListView($choiceList, $options); + $builder->setAttribute('choice_list_view', $choiceListView); + + // Check if the choices already contain the empty value + // Only add the placeholder option if this is not the case + if (null !== $options['placeholder'] && 0 === \count($choiceList->getChoicesForValues(['']))) { + $placeholderView = new ChoiceView(null, '', $options['placeholder'], $options['placeholder_attr']); + + // "placeholder" is a reserved name + $this->addSubForm($builder, 'placeholder', $placeholderView, $options); + } + + $this->addSubForms($builder, $choiceListView->preferredChoices, $options); + $this->addSubForms($builder, $choiceListView->choices, $options); + } + + if ($options['expanded'] || $options['multiple']) { + // Make sure that scalar, submitted values are converted to arrays + // which can be submitted to the checkboxes/radio buttons + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($choiceList, $options, &$unknownValues) { + /** @var PreSubmitEvent $event */ + $form = $event->getForm(); + $data = $event->getData(); + + // Since the type always use mapper an empty array will not be + // considered as empty in Form::submit(), we need to evaluate + // empty data here so its value is submitted to sub forms + if (null === $data) { + $emptyData = $form->getConfig()->getEmptyData(); + $data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData; + } + + // Convert the submitted data to a string, if scalar, before + // casting it to an array + if (!\is_array($data)) { + if ($options['multiple']) { + throw new TransformationFailedException('Expected an array.'); + } + + $data = (array) (string) $data; + } + + // A map from submitted values to integers + $valueMap = array_flip($data); + + // Make a copy of the value map to determine whether any unknown + // values were submitted + $unknownValues = $valueMap; + + // Reconstruct the data as mapping from child names to values + $knownValues = []; + + if ($options['expanded']) { + /** @var FormInterface $child */ + foreach ($form as $child) { + $value = $child->getConfig()->getOption('value'); + + // Add the value to $data with the child's name as key + if (isset($valueMap[$value])) { + $knownValues[$child->getName()] = $value; + unset($unknownValues[$value]); + continue; + } else { + $knownValues[$child->getName()] = null; + } + } + } else { + foreach ($choiceList->getChoicesForValues($data) as $key => $choice) { + $knownValues[] = $data[$key]; + unset($unknownValues[$data[$key]]); + } + } + + // The empty value is always known, independent of whether a + // field exists for it or not + unset($unknownValues['']); + + // Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below) + if (\count($unknownValues) > 0 && !$options['multiple']) { + throw new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues)))); + } + + $event->setData($knownValues); + }); + } + + if ($options['multiple']) { + $messageTemplate = $options['invalid_message'] ?? 'The value {{ value }} is not valid.'; + $translator = $this->translator; + + $builder->addEventListener(FormEvents::POST_SUBMIT, static function (FormEvent $event) use (&$unknownValues, $messageTemplate, $translator) { + // Throw exception if unknown values were submitted + if (\count($unknownValues) > 0) { + $form = $event->getForm(); + + $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : (\is_array($form->getViewData()) ? implode('", "', array_keys($unknownValues)) : \gettype($form->getViewData())); + + if ($translator) { + $message = $translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators'); + } else { + $message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]); + } + + $form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString)))); + } + }); + + // tag without "multiple" option or list of radio inputs + $builder->addViewTransformer(new ChoiceToValueTransformer($choiceList)); + } + + if ($options['multiple'] && $options['by_reference']) { + // Make sure the collection created during the client->norm + // transformation is merged back into the original collection + $builder->addEventSubscriber(new MergeCollectionListener(true, true)); + } + + // To avoid issues when the submitted choices are arrays (i.e. array to string conversions), + // we have to ensure that all elements of the submitted choice data are NULL, strings or ints. + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) { + $data = $event->getData(); + + if (!\is_array($data)) { + return; + } + + foreach ($data as $v) { + if (null !== $v && !\is_string($v) && !\is_int($v)) { + throw new TransformationFailedException('All choices submitted must be NULL, strings or ints.'); + } + } + }, 256); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $choiceTranslationDomain = $options['choice_translation_domain']; + if ($view->parent && null === $choiceTranslationDomain) { + $choiceTranslationDomain = $view->vars['translation_domain']; + } + + /** @var ChoiceListInterface $choiceList */ + $choiceList = $form->getConfig()->getAttribute('choice_list'); + + /** @var ChoiceListView $choiceListView */ + $choiceListView = $form->getConfig()->hasAttribute('choice_list_view') + ? $form->getConfig()->getAttribute('choice_list_view') + : $this->createChoiceListView($choiceList, $options); + + $view->vars = array_replace($view->vars, [ + 'multiple' => $options['multiple'], + 'expanded' => $options['expanded'], + 'preferred_choices' => $choiceListView->preferredChoices, + 'choices' => $choiceListView->choices, + 'separator' => '-------------------', + 'placeholder' => null, + 'placeholder_attr' => [], + 'choice_translation_domain' => $choiceTranslationDomain, + 'choice_translation_parameters' => $options['choice_translation_parameters'], + ]); + + // The decision, whether a choice is selected, is potentially done + // thousand of times during the rendering of a template. Provide a + // closure here that is optimized for the value of the form, to + // avoid making the type check inside the closure. + if ($options['multiple']) { + $view->vars['is_selected'] = static fn ($choice, array $values) => \in_array($choice, $values, true); + } else { + $view->vars['is_selected'] = static fn ($choice, $value) => $choice === $value; + } + + // Check if the choices already contain the empty value + $view->vars['placeholder_in_choices'] = $choiceListView->hasPlaceholder(); + + // Only add the empty value option if this is not the case + if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) { + $view->vars['placeholder'] = $options['placeholder']; + $view->vars['placeholder_attr'] = $options['placeholder_attr']; + } + + if ($options['multiple'] && !$options['expanded']) { + // Add "[]" to the name in case a select tag with multiple options is + // displayed. Otherwise only one of the selected options is sent in the + // POST request. + $view->vars['full_name'] .= '[]'; + } + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $view->vars['duplicate_preferred_choices'] = $options['duplicate_preferred_choices']; + + if ($options['expanded']) { + // Radio buttons should have the same name as the parent + $childName = $view->vars['full_name']; + + // Checkboxes should append "[]" to allow multiple selection + if ($options['multiple']) { + $childName .= '[]'; + } + + foreach ($view as $childView) { + $childView->vars['full_name'] = $childName; + } + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $emptyData = static function (Options $options) { + if ($options['expanded'] && !$options['multiple']) { + return null; + } + + if ($options['multiple']) { + return []; + } + + return ''; + }; + + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; + + $placeholderNormalizer = static function (Options $options, $placeholder) { + if ($options['multiple']) { + // never use an empty value for this case + return null; + } elseif ($options['required'] && ($options['expanded'] || isset($options['attr']['size']) && $options['attr']['size'] > 1)) { + // placeholder for required radio buttons or a select with size > 1 does not make sense + return null; + } elseif (false === $placeholder) { + // an empty value should be added but the user decided otherwise + return null; + } elseif ($options['expanded'] && '' === $placeholder) { + // never use an empty label for radio buttons + return 'None'; + } + + // empty value has been set explicitly + return $placeholder; + }; + + $compound = static fn (Options $options) => $options['expanded']; + + $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { + if (true === $choiceTranslationDomain) { + return $options['translation_domain']; + } + + return $choiceTranslationDomain; + }; + + $resolver->setDefaults([ + 'multiple' => false, + 'expanded' => false, + 'choices' => [], + 'choice_filter' => null, + 'choice_loader' => null, + 'choice_label' => null, + 'choice_name' => null, + 'choice_value' => null, + 'choice_attr' => null, + 'choice_translation_parameters' => [], + 'preferred_choices' => [], + 'duplicate_preferred_choices' => true, + 'group_by' => null, + 'empty_data' => $emptyData, + 'placeholder' => $placeholderDefault, + 'placeholder_attr' => [], + 'error_bubbling' => false, + 'compound' => $compound, + // The view data is always a string or an array of strings, + // even if the "data" option is manually set to an object. + // See https://github.com/symfony/symfony/pull/5582 + 'data_class' => null, + 'choice_translation_domain' => true, + 'trim' => false, + 'invalid_message' => 'The selected choice is invalid.', + ]); + + $resolver->setNormalizer('placeholder', $placeholderNormalizer); + $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); + + $resolver->setAllowedTypes('choices', ['null', 'array', \Traversable::class]); + $resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']); + $resolver->setAllowedTypes('choice_loader', ['null', ChoiceLoaderInterface::class, ChoiceLoader::class]); + $resolver->setAllowedTypes('choice_filter', ['null', 'callable', 'string', PropertyPath::class, ChoiceFilter::class]); + $resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class, ChoiceLabel::class]); + $resolver->setAllowedTypes('choice_name', ['null', 'callable', 'string', PropertyPath::class, ChoiceFieldName::class]); + $resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', PropertyPath::class, ChoiceValue::class]); + $resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', PropertyPath::class, ChoiceAttr::class]); + $resolver->setAllowedTypes('choice_translation_parameters', ['null', 'array', 'callable', ChoiceTranslationParameters::class]); + $resolver->setAllowedTypes('placeholder_attr', ['array']); + $resolver->setAllowedTypes('preferred_choices', ['array', \Traversable::class, 'callable', 'string', PropertyPath::class, PreferredChoice::class]); + $resolver->setAllowedTypes('duplicate_preferred_choices', 'bool'); + $resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]); + } + + public function getBlockPrefix(): string + { + return 'choice'; + } + + /** + * Adds the sub fields for an expanded choice field. + */ + private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options): void + { + foreach ($choiceViews as $name => $choiceView) { + // Flatten groups + if (\is_array($choiceView)) { + $this->addSubForms($builder, $choiceView, $options); + continue; + } + + if ($choiceView instanceof ChoiceGroupView) { + $this->addSubForms($builder, $choiceView->choices, $options); + continue; + } + + $this->addSubForm($builder, $name, $choiceView, $options); + } + } + + private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceView $choiceView, array $options): void + { + $choiceOpts = [ + 'value' => $choiceView->value, + 'label' => $choiceView->label, + 'label_html' => $options['label_html'], + 'attr' => $choiceView->attr, + 'label_translation_parameters' => $choiceView->labelTranslationParameters, + 'translation_domain' => $options['choice_translation_domain'], + 'block_name' => 'entry', + ]; + + if ($options['multiple']) { + $choiceType = CheckboxType::class; + // The user can check 0 or more checkboxes. If required + // is true, they are required to check all of them. + $choiceOpts['required'] = false; + } else { + $choiceType = RadioType::class; + } + + $builder->add($name, $choiceType, $choiceOpts); + } + + private function createChoiceList(array $options): ChoiceListInterface + { + if (null !== $options['choice_loader']) { + return $this->choiceListFactory->createListFromLoader( + $options['choice_loader'], + $options['choice_value'], + $options['choice_filter'] + ); + } + + // Harden against NULL values (like in EntityType and ModelType) + $choices = null !== $options['choices'] ? $options['choices'] : []; + + return $this->choiceListFactory->createListFromChoices( + $choices, + $options['choice_value'], + $options['choice_filter'] + ); + } + + private function createChoiceListView(ChoiceListInterface $choiceList, array $options): ChoiceListView + { + return $this->choiceListFactory->createView( + $choiceList, + $options['preferred_choices'], + $options['choice_label'], + $options['choice_name'], + $options['group_by'], + $options['choice_attr'], + $options['choice_translation_parameters'], + $options['duplicate_preferred_choices'], + ); + } +} diff --git a/lib/symfony/form/Extension/Core/Type/CollectionType.php b/lib/symfony/form/Extension/Core/Type/CollectionType.php new file mode 100644 index 0000000000..0216e61dd5 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/CollectionType.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class CollectionType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $resizePrototypeOptions = null; + if ($options['allow_add'] && $options['prototype']) { + $resizePrototypeOptions = array_replace($options['entry_options'], $options['prototype_options']); + $prototypeOptions = array_replace([ + 'required' => $options['required'], + 'label' => $options['prototype_name'].'label__', + ], $resizePrototypeOptions); + + if (null !== $options['prototype_data']) { + $prototypeOptions['data'] = $options['prototype_data']; + } + + $prototype = $builder->create($options['prototype_name'], $options['entry_type'], $prototypeOptions); + $builder->setAttribute('prototype', $prototype->getForm()); + } + + $resizeListener = new ResizeFormListener( + $options['entry_type'], + $options['entry_options'], + $options['allow_add'], + $options['allow_delete'], + $options['delete_empty'], + $resizePrototypeOptions + ); + + $builder->addEventSubscriber($resizeListener); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars = array_replace($view->vars, [ + 'allow_add' => $options['allow_add'], + 'allow_delete' => $options['allow_delete'], + ]); + + if ($form->getConfig()->hasAttribute('prototype')) { + $prototype = $form->getConfig()->getAttribute('prototype'); + $view->vars['prototype'] = $prototype->setParent($form)->createView($view); + } + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $prefixOffset = -2; + // check if the entry type also defines a block prefix + /** @var FormInterface $entry */ + foreach ($form as $entry) { + if ($entry->getConfig()->getOption('block_prefix')) { + --$prefixOffset; + } + + break; + } + + foreach ($view as $entryView) { + array_splice($entryView->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry'); + } + + /** @var FormInterface $prototype */ + if ($prototype = $form->getConfig()->getAttribute('prototype')) { + if ($view->vars['prototype']->vars['multipart']) { + $view->vars['multipart'] = true; + } + + if ($prefixOffset > -3 && $prototype->getConfig()->getOption('block_prefix')) { + --$prefixOffset; + } + + array_splice($view->vars['prototype']->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry'); + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $entryOptionsNormalizer = static function (Options $options, $value) { + $value['block_name'] = 'entry'; + + return $value; + }; + + $resolver->setDefaults([ + 'allow_add' => false, + 'allow_delete' => false, + 'prototype' => true, + 'prototype_data' => null, + 'prototype_name' => '__name__', + 'entry_type' => TextType::class, + 'entry_options' => [], + 'prototype_options' => [], + 'delete_empty' => false, + 'invalid_message' => 'The collection is invalid.', + ]); + + $resolver->setNormalizer('entry_options', $entryOptionsNormalizer); + + $resolver->setAllowedTypes('delete_empty', ['bool', 'callable']); + $resolver->setAllowedTypes('prototype_options', 'array'); + } + + public function getBlockPrefix(): string + { + return 'collection'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/ColorType.php b/lib/symfony/form/Extension/Core/Type/ColorType.php new file mode 100644 index 0000000000..71df9edd8c --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/ColorType.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +class ColorType extends AbstractType +{ + /** + * @see https://www.w3.org/TR/html52/sec-forms.html#color-state-typecolor + */ + private const HTML5_PATTERN = '/^#[0-9a-f]{6}$/i'; + + private ?TranslatorInterface $translator; + + public function __construct(?TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['html5']) { + return; + } + + $translator = $this->translator; + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($translator): void { + $value = $event->getData(); + if (null === $value || '' === $value) { + return; + } + + if (\is_string($value) && preg_match(self::HTML5_PATTERN, $value)) { + return; + } + + $messageTemplate = 'This value is not a valid HTML5 color.'; + $messageParameters = [ + '{{ value }}' => \is_scalar($value) ? (string) $value : \gettype($value), + ]; + $message = $translator?->trans($messageTemplate, $messageParameters, 'validators') ?? $messageTemplate; + + $event->getForm()->addError(new FormError($message, $messageTemplate, $messageParameters)); + }); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'html5' => false, + 'invalid_message' => 'Please select a valid color.', + ]); + + $resolver->setAllowedTypes('html5', 'bool'); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'color'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/CountryType.php b/lib/symfony/form/Extension/Core/Type/CountryType.php new file mode 100644 index 0000000000..94d80a320f --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/CountryType.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Intl\Countries; +use Symfony\Component\Intl\Intl; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class CountryType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'choice_loader' => function (Options $options) { + if (!class_exists(Intl::class)) { + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + } + + $choiceTranslationLocale = $options['choice_translation_locale']; + $alpha3 = $options['alpha3']; + + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale))), [$choiceTranslationLocale, $alpha3]); + }, + 'choice_translation_domain' => false, + 'choice_translation_locale' => null, + 'alpha3' => false, + 'invalid_message' => 'Please select a valid country.', + ]); + + $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); + $resolver->setAllowedTypes('alpha3', 'bool'); + } + + public function getParent(): ?string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'country'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/CurrencyType.php b/lib/symfony/form/Extension/Core/Type/CurrencyType.php new file mode 100644 index 0000000000..ea4c39b235 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/CurrencyType.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Intl\Currencies; +use Symfony\Component\Intl\Intl; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class CurrencyType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'choice_loader' => function (Options $options) { + if (!class_exists(Intl::class)) { + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + } + + $choiceTranslationLocale = $options['choice_translation_locale']; + + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Currencies::getNames($choiceTranslationLocale))), $choiceTranslationLocale); + }, + 'choice_translation_domain' => false, + 'choice_translation_locale' => null, + 'invalid_message' => 'Please select a valid currency.', + ]); + + $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); + } + + public function getParent(): ?string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'currency'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/DateIntervalType.php b/lib/symfony/form/Extension/Core/Type/DateIntervalType.php new file mode 100644 index 0000000000..655ef6682f --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/DateIntervalType.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Steffen Roßkamp + */ +class DateIntervalType extends AbstractType +{ + private const TIME_PARTS = [ + 'years', + 'months', + 'weeks', + 'days', + 'hours', + 'minutes', + 'seconds', + ]; + private const WIDGETS = [ + 'text' => TextType::class, + 'integer' => IntegerType::class, + 'choice' => ChoiceType::class, + ]; + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['with_years'] && !$options['with_months'] && !$options['with_weeks'] && !$options['with_days'] && !$options['with_hours'] && !$options['with_minutes'] && !$options['with_seconds']) { + throw new InvalidConfigurationException('You must enable at least one interval field.'); + } + if ($options['with_invert'] && 'single_text' === $options['widget']) { + throw new InvalidConfigurationException('The single_text widget does not support invertible intervals.'); + } + if ($options['with_weeks'] && $options['with_days']) { + throw new InvalidConfigurationException('You cannot enable weeks and days fields together.'); + } + $format = 'P'; + $parts = []; + if ($options['with_years']) { + $format .= '%yY'; + $parts[] = 'years'; + } + if ($options['with_months']) { + $format .= '%mM'; + $parts[] = 'months'; + } + if ($options['with_weeks']) { + $format .= '%wW'; + $parts[] = 'weeks'; + } + if ($options['with_days']) { + $format .= '%dD'; + $parts[] = 'days'; + } + if ($options['with_hours'] || $options['with_minutes'] || $options['with_seconds']) { + $format .= 'T'; + } + if ($options['with_hours']) { + $format .= '%hH'; + $parts[] = 'hours'; + } + if ($options['with_minutes']) { + $format .= '%iM'; + $parts[] = 'minutes'; + } + if ($options['with_seconds']) { + $format .= '%sS'; + $parts[] = 'seconds'; + } + if ($options['with_invert']) { + $parts[] = 'invert'; + } + if ('single_text' === $options['widget']) { + $builder->addViewTransformer(new DateIntervalToStringTransformer($format)); + } else { + foreach (self::TIME_PARTS as $part) { + if ($options['with_'.$part]) { + $childOptions = [ + 'error_bubbling' => true, + 'label' => $options['labels'][$part], + // Append generic carry-along options + 'required' => $options['required'], + 'translation_domain' => $options['translation_domain'], + // when compound the array entries are ignored, we need to cascade the configuration here + 'empty_data' => $options['empty_data'][$part] ?? null, + ]; + if ('choice' === $options['widget']) { + $childOptions['choice_translation_domain'] = false; + $childOptions['choices'] = $options[$part]; + $childOptions['placeholder'] = $options['placeholder'][$part]; + } + $childForm = $builder->create($part, self::WIDGETS[$options['widget']], $childOptions); + if ('integer' === $options['widget']) { + $childForm->addModelTransformer( + new ReversedTransformer( + new IntegerToLocalizedStringTransformer() + ) + ); + } + $builder->add($childForm); + } + } + if ($options['with_invert']) { + $builder->add('invert', CheckboxType::class, [ + 'label' => $options['labels']['invert'], + 'error_bubbling' => true, + 'required' => false, + 'translation_domain' => $options['translation_domain'], + ]); + } + $builder->addViewTransformer(new DateIntervalToArrayTransformer($parts, 'text' === $options['widget'])); + } + if ('string' === $options['input']) { + $builder->addModelTransformer( + new ReversedTransformer( + new DateIntervalToStringTransformer($format) + ) + ); + } elseif ('array' === $options['input']) { + $builder->addModelTransformer( + new ReversedTransformer( + new DateIntervalToArrayTransformer($parts) + ) + ); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $vars = [ + 'widget' => $options['widget'], + 'with_invert' => $options['with_invert'], + ]; + foreach (self::TIME_PARTS as $part) { + $vars['with_'.$part] = $options['with_'.$part]; + } + $view->vars = array_replace($view->vars, $vars); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; + $emptyData = static fn (Options $options) => 'single_text' === $options['widget'] ? '' : []; + + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; + + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { + if (\is_array($placeholder)) { + $default = $placeholderDefault($options); + + return array_merge(array_fill_keys(self::TIME_PARTS, $default), $placeholder); + } + + return array_fill_keys(self::TIME_PARTS, $placeholder); + }; + + $labelsNormalizer = static fn (Options $options, array $labels) => array_replace([ + 'years' => null, + 'months' => null, + 'days' => null, + 'weeks' => null, + 'hours' => null, + 'minutes' => null, + 'seconds' => null, + 'invert' => 'Negative interval', + ], array_filter($labels, static fn ($label) => null !== $label)); + + $resolver->setDefaults([ + 'with_years' => true, + 'with_months' => true, + 'with_days' => true, + 'with_weeks' => false, + 'with_hours' => false, + 'with_minutes' => false, + 'with_seconds' => false, + 'with_invert' => false, + 'years' => range(0, 100), + 'months' => range(0, 12), + 'weeks' => range(0, 52), + 'days' => range(0, 31), + 'hours' => range(0, 24), + 'minutes' => range(0, 60), + 'seconds' => range(0, 60), + 'widget' => 'choice', + 'input' => 'dateinterval', + 'placeholder' => $placeholderDefault, + 'by_reference' => true, + 'error_bubbling' => false, + // If initialized with a \DateInterval object, FormType initializes + // this option to "\DateInterval". Since the internal, normalized + // representation is not \DateInterval, but an array, we need to unset + // this option. + 'data_class' => null, + 'compound' => $compound, + 'empty_data' => $emptyData, + 'labels' => [], + 'invalid_message' => 'Please choose a valid date interval.', + ]); + $resolver->setNormalizer('placeholder', $placeholderNormalizer); + $resolver->setNormalizer('labels', $labelsNormalizer); + + $resolver->setAllowedValues( + 'input', + [ + 'dateinterval', + 'string', + 'array', + ] + ); + $resolver->setAllowedValues( + 'widget', + [ + 'single_text', + 'text', + 'integer', + 'choice', + ] + ); + // Don't clone \DateInterval classes, as i.e. format() + // does not work after that + $resolver->setAllowedValues('by_reference', true); + + $resolver->setAllowedTypes('years', 'array'); + $resolver->setAllowedTypes('months', 'array'); + $resolver->setAllowedTypes('weeks', 'array'); + $resolver->setAllowedTypes('days', 'array'); + $resolver->setAllowedTypes('hours', 'array'); + $resolver->setAllowedTypes('minutes', 'array'); + $resolver->setAllowedTypes('seconds', 'array'); + $resolver->setAllowedTypes('with_years', 'bool'); + $resolver->setAllowedTypes('with_months', 'bool'); + $resolver->setAllowedTypes('with_weeks', 'bool'); + $resolver->setAllowedTypes('with_days', 'bool'); + $resolver->setAllowedTypes('with_hours', 'bool'); + $resolver->setAllowedTypes('with_minutes', 'bool'); + $resolver->setAllowedTypes('with_seconds', 'bool'); + $resolver->setAllowedTypes('with_invert', 'bool'); + $resolver->setAllowedTypes('labels', 'array'); + } + + public function getBlockPrefix(): string + { + return 'dateinterval'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/DateTimeType.php b/lib/symfony/form/Extension/Core/Type/DateTimeType.php new file mode 100644 index 0000000000..fe0b2934a3 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/DateTimeType.php @@ -0,0 +1,357 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class DateTimeType extends AbstractType +{ + public const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM; + public const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM; + + /** + * The HTML5 datetime-local format as defined in + * http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local. + */ + public const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + + private const ACCEPTED_FORMATS = [ + \IntlDateFormatter::FULL, + \IntlDateFormatter::LONG, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::SHORT, + ]; + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $parts = ['year', 'month', 'day', 'hour']; + $dateParts = ['year', 'month', 'day']; + $timeParts = ['hour']; + + if ($options['with_minutes']) { + $parts[] = 'minute'; + $timeParts[] = 'minute'; + } + + if ($options['with_seconds']) { + $parts[] = 'second'; + $timeParts[] = 'second'; + } + + $dateFormat = \is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT; + $timeFormat = self::DEFAULT_TIME_FORMAT; + $calendar = \IntlDateFormatter::GREGORIAN; + $pattern = \is_string($options['format']) ? $options['format'] : null; + + if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) { + throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.'); + } + + if ('single_text' === $options['widget']) { + if (self::HTML5_FORMAT === $pattern) { + $builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer( + $options['model_timezone'], + $options['view_timezone'], + $options['with_seconds'] + )); + } else { + $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( + $options['model_timezone'], + $options['view_timezone'], + $dateFormat, + $timeFormat, + $calendar, + $pattern + )); + } + } else { + // when the form is compound the entries of the array are ignored in favor of children data + // so we need to handle the cascade setting here + $emptyData = $builder->getEmptyData() ?: []; + // Only pass a subset of the options to children + $dateOptions = array_intersect_key($options, array_flip([ + 'years', + 'months', + 'days', + 'placeholder', + 'choice_translation_domain', + 'required', + 'translation_domain', + 'html5', + 'invalid_message', + 'invalid_message_parameters', + ])); + + if ($emptyData instanceof \Closure) { + $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) { + $emptyData = $emptyData($form->getParent()); + + return $emptyData[$option] ?? ''; + }; + + $dateOptions['empty_data'] = $lazyEmptyData('date'); + } elseif (isset($emptyData['date'])) { + $dateOptions['empty_data'] = $emptyData['date']; + } + + $timeOptions = array_intersect_key($options, array_flip([ + 'hours', + 'minutes', + 'seconds', + 'with_minutes', + 'with_seconds', + 'placeholder', + 'choice_translation_domain', + 'required', + 'translation_domain', + 'html5', + 'invalid_message', + 'invalid_message_parameters', + ])); + + if ($emptyData instanceof \Closure) { + $timeOptions['empty_data'] = $lazyEmptyData('time'); + } elseif (isset($emptyData['time'])) { + $timeOptions['empty_data'] = $emptyData['time']; + } + + if (false === $options['label']) { + $dateOptions['label'] = false; + $timeOptions['label'] = false; + } + + $dateOptions['widget'] = $options['date_widget'] ?? $options['widget'] ?? 'choice'; + $timeOptions['widget'] = $options['time_widget'] ?? $options['widget'] ?? 'choice'; + + if (null !== $options['date_label']) { + $dateOptions['label'] = $options['date_label']; + } + + if (null !== $options['time_label']) { + $timeOptions['label'] = $options['time_label']; + } + + if (null !== $options['date_format']) { + $dateOptions['format'] = $options['date_format']; + } + + $dateOptions['input'] = $timeOptions['input'] = 'array'; + $dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true; + + $builder + ->addViewTransformer(new DataTransformerChain([ + new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts), + new ArrayToPartsTransformer([ + 'date' => $dateParts, + 'time' => $timeParts, + ]), + ])) + ->add('date', DateType::class, $dateOptions) + ->add('time', TimeType::class, $timeOptions) + ; + } + + if ('datetime_immutable' === $options['input']) { + $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer()); + } elseif ('string' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format']) + )); + } elseif ('timestamp' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone']) + )); + } elseif ('array' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts) + )); + } + + if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $date = $event->getData(); + + if (!$date instanceof \DateTimeInterface) { + return; + } + + if ($date->getTimezone()->getName() !== $options['model_timezone']) { + trigger_deprecation('symfony/form', '6.4', \sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is deprecated.', $date::class, $date->getTimezone()->getName(), $options['model_timezone'])); + // throw new LogicException(sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', $date::class, $date->getTimezone()->getName(), $options['model_timezone'])); + } + }); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['widget'] = $options['widget']; + + // Change the input to an HTML5 datetime input if + // * the widget is set to "single_text" + // * the format matches the one expected by HTML5 + // * the html5 is set to true + if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) { + $view->vars['type'] = 'datetime-local'; + + // we need to force the browser to display the seconds by + // adding the HTML attribute step if not already defined. + // Otherwise the browser will not display and so not send the seconds + // therefore the value will always be considered as invalid. + if (!isset($view->vars['attr']['step'])) { + if ($options['with_seconds']) { + $view->vars['attr']['step'] = 1; + } elseif (!$options['with_minutes']) { + $view->vars['attr']['step'] = 3600; + } + } + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; + + $resolver->setDefaults([ + 'input' => 'datetime', + 'model_timezone' => null, + 'view_timezone' => null, + 'format' => self::HTML5_FORMAT, + 'date_format' => null, + 'widget' => null, + 'date_widget' => null, + 'time_widget' => null, + 'with_minutes' => true, + 'with_seconds' => false, + 'html5' => true, + // Don't modify \DateTime classes by reference, we treat + // them like immutable value objects + 'by_reference' => false, + 'error_bubbling' => false, + // If initialized with a \DateTime object, FormType initializes + // this option to "\DateTime". Since the internal, normalized + // representation is not \DateTime, but an array, we need to unset + // this option. + 'data_class' => null, + 'compound' => $compound, + 'date_label' => null, + 'time_label' => null, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', + 'input_format' => 'Y-m-d H:i:s', + 'invalid_message' => 'Please enter a valid date and time.', + ]); + + // Don't add some defaults in order to preserve the defaults + // set in DateType and TimeType + $resolver->setDefined([ + 'placeholder', + 'choice_translation_domain', + 'years', + 'months', + 'days', + 'hours', + 'minutes', + 'seconds', + ]); + + $resolver->setAllowedValues('input', [ + 'datetime', + 'datetime_immutable', + 'string', + 'timestamp', + 'array', + ]); + $resolver->setAllowedValues('date_widget', [ + null, // inherit default from DateType + 'single_text', + 'text', + 'choice', + ]); + $resolver->setAllowedValues('time_widget', [ + null, // inherit default from TimeType + 'single_text', + 'text', + 'choice', + ]); + // This option will overwrite "date_widget" and "time_widget" options + $resolver->setAllowedValues('widget', [ + null, // default, don't overwrite options + 'single_text', + 'text', + 'choice', + ]); + + $resolver->setAllowedTypes('input_format', 'string'); + + $resolver->setNormalizer('date_format', static function (Options $options, $dateFormat) { + if (null !== $dateFormat && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) { + throw new LogicException(\sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.', self::class)); + } + + return $dateFormat; + }); + $resolver->setNormalizer('widget', static function (Options $options, $widget) { + if ('single_text' === $widget) { + if (null !== $options['date_widget']) { + throw new LogicException(\sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class)); + } + if (null !== $options['time_widget']) { + throw new LogicException(\sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class)); + } + } elseif (null === $widget && null === $options['date_widget'] && null === $options['time_widget']) { + trigger_deprecation('symfony/form', '6.3', 'Not configuring the "widget" option of form type "datetime" is deprecated. It will default to "single_text" in Symfony 7.0.'); + // return 'single_text'; + } + + return $widget; + }); + $resolver->setNormalizer('html5', static function (Options $options, $html5) { + if ($html5 && self::HTML5_FORMAT !== $options['format']) { + throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class)); + } + + return $html5; + }); + } + + public function getBlockPrefix(): string + { + return 'datetime'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/DateType.php b/lib/symfony/form/Extension/Core/Type/DateType.php new file mode 100644 index 0000000000..a479f7673b --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/DateType.php @@ -0,0 +1,409 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class DateType extends AbstractType +{ + public const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM; + public const HTML5_FORMAT = 'yyyy-MM-dd'; + + private const ACCEPTED_FORMATS = [ + \IntlDateFormatter::FULL, + \IntlDateFormatter::LONG, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::SHORT, + ]; + + private const WIDGETS = [ + 'text' => TextType::class, + 'choice' => ChoiceType::class, + ]; + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $dateFormat = \is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT; + $timeFormat = \IntlDateFormatter::NONE; + $calendar = \IntlDateFormatter::GREGORIAN; + $pattern = \is_string($options['format']) ? $options['format'] : ''; + + if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) { + throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.'); + } + + if ('single_text' === $options['widget']) { + if ('' !== $pattern && !str_contains($pattern, 'y') && !str_contains($pattern, 'M') && !str_contains($pattern, 'd')) { + throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern)); + } + + $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( + $options['model_timezone'], + $options['view_timezone'], + $dateFormat, + $timeFormat, + $calendar, + $pattern + )); + } else { + if ('' !== $pattern && (!str_contains($pattern, 'y') || !str_contains($pattern, 'M') || !str_contains($pattern, 'd'))) { + throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern)); + } + + $yearOptions = $monthOptions = $dayOptions = [ + 'error_bubbling' => true, + 'empty_data' => '', + ]; + // when the form is compound the entries of the array are ignored in favor of children data + // so we need to handle the cascade setting here + $emptyData = $builder->getEmptyData() ?: []; + + if ($emptyData instanceof \Closure) { + $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) { + $emptyData = $emptyData($form->getParent()); + + return $emptyData[$option] ?? ''; + }; + + $yearOptions['empty_data'] = $lazyEmptyData('year'); + $monthOptions['empty_data'] = $lazyEmptyData('month'); + $dayOptions['empty_data'] = $lazyEmptyData('day'); + } else { + if (isset($emptyData['year'])) { + $yearOptions['empty_data'] = $emptyData['year']; + } + if (isset($emptyData['month'])) { + $monthOptions['empty_data'] = $emptyData['month']; + } + if (isset($emptyData['day'])) { + $dayOptions['empty_data'] = $emptyData['day']; + } + } + + if (isset($options['invalid_message'])) { + $dayOptions['invalid_message'] = $options['invalid_message']; + $monthOptions['invalid_message'] = $options['invalid_message']; + $yearOptions['invalid_message'] = $options['invalid_message']; + } + + if (isset($options['invalid_message_parameters'])) { + $dayOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + $monthOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + } + + $formatter = new \IntlDateFormatter( + \Locale::getDefault(), + $dateFormat, + $timeFormat, + // see https://bugs.php.net/66323 + class_exists(\IntlTimeZone::class, false) ? \IntlTimeZone::createDefault() : null, + $calendar, + $pattern + ); + + // new \IntlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323 + if (!$formatter) { + throw new InvalidOptionsException(intl_get_error_message(), intl_get_error_code()); + } + + $formatter->setLenient(false); + + if ('choice' === $options['widget']) { + // Only pass a subset of the options to children + $yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years'])); + $yearOptions['placeholder'] = $options['placeholder']['year']; + $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year']; + $monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months'])); + $monthOptions['placeholder'] = $options['placeholder']['month']; + $monthOptions['choice_translation_domain'] = $options['choice_translation_domain']['month']; + $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days'])); + $dayOptions['placeholder'] = $options['placeholder']['day']; + $dayOptions['choice_translation_domain'] = $options['choice_translation_domain']['day']; + } + + // Append generic carry-along options + foreach (['required', 'translation_domain'] as $passOpt) { + $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt]; + } + + $builder + ->add('year', self::WIDGETS[$options['widget']], $yearOptions) + ->add('month', self::WIDGETS[$options['widget']], $monthOptions) + ->add('day', self::WIDGETS[$options['widget']], $dayOptions) + ->addViewTransformer(new DateTimeToArrayTransformer( + $options['model_timezone'], $options['view_timezone'], ['year', 'month', 'day'] + )) + ->setAttribute('formatter', $formatter) + ; + } + + if ('datetime_immutable' === $options['input']) { + $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer()); + } elseif ('string' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format']) + )); + } elseif ('timestamp' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone']) + )); + } elseif ('array' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], ['year', 'month', 'day']) + )); + } + + if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $date = $event->getData(); + + if (!$date instanceof \DateTimeInterface) { + return; + } + + if ($date->getTimezone()->getName() !== $options['model_timezone']) { + trigger_deprecation('symfony/form', '6.4', \sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is deprecated.', $date::class, $date->getTimezone()->getName(), $options['model_timezone'])); + // throw new LogicException(sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', $date::class, $date->getTimezone()->getName(), $options['model_timezone'])); + } + }); + } + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $view->vars['widget'] = $options['widget']; + + // Change the input to an HTML5 date input if + // * the widget is set to "single_text" + // * the format matches the one expected by HTML5 + // * the html5 is set to true + if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) { + $view->vars['type'] = 'date'; + } + + if ($form->getConfig()->hasAttribute('formatter')) { + $pattern = $form->getConfig()->getAttribute('formatter')->getPattern(); + + // remove special characters unless the format was explicitly specified + if (!\is_string($options['format'])) { + // remove quoted strings first + $pattern = preg_replace('/\'[^\']+\'/', '', $pattern); + + // remove remaining special chars + $pattern = preg_replace('/[^yMd]+/', '', $pattern); + } + + // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy) + // lookup various formats at http://userguide.icu-project.org/formatparse/datetime + if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) { + $pattern = preg_replace(['/y+/', '/M+/', '/d+/'], ['{{ year }}', '{{ month }}', '{{ day }}'], $pattern); + } else { + // default fallback + $pattern = '{{ year }}{{ month }}{{ day }}'; + } + + $view->vars['date_pattern'] = $pattern; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; + + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; + + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { + if (\is_array($placeholder)) { + $default = $placeholderDefault($options); + + return array_merge( + ['year' => $default, 'month' => $default, 'day' => $default], + $placeholder + ); + } + + return [ + 'year' => $placeholder, + 'month' => $placeholder, + 'day' => $placeholder, + ]; + }; + + $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { + if (\is_array($choiceTranslationDomain)) { + $default = false; + + return array_replace( + ['year' => $default, 'month' => $default, 'day' => $default], + $choiceTranslationDomain + ); + } + + return [ + 'year' => $choiceTranslationDomain, + 'month' => $choiceTranslationDomain, + 'day' => $choiceTranslationDomain, + ]; + }; + + $format = static fn (Options $options) => 'single_text' === $options['widget'] ? self::HTML5_FORMAT : self::DEFAULT_FORMAT; + + $resolver->setDefaults([ + 'years' => range((int) date('Y') - 5, (int) date('Y') + 5), + 'months' => range(1, 12), + 'days' => range(1, 31), + 'widget' => static function (Options $options) { + trigger_deprecation('symfony/form', '6.3', 'Not configuring the "widget" option of form type "date" is deprecated. It will default to "single_text" in Symfony 7.0.'); + + return 'choice'; + }, + 'input' => 'datetime', + 'format' => $format, + 'model_timezone' => null, + 'view_timezone' => null, + 'placeholder' => $placeholderDefault, + 'html5' => true, + // Don't modify \DateTime classes by reference, we treat + // them like immutable value objects + 'by_reference' => false, + 'error_bubbling' => false, + // If initialized with a \DateTime object, FormType initializes + // this option to "\DateTime". Since the internal, normalized + // representation is not \DateTime, but an array, we need to unset + // this option. + 'data_class' => null, + 'compound' => $compound, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', + 'choice_translation_domain' => false, + 'input_format' => 'Y-m-d', + 'invalid_message' => 'Please enter a valid date.', + ]); + + $resolver->setNormalizer('placeholder', $placeholderNormalizer); + $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); + + $resolver->setAllowedValues('input', [ + 'datetime', + 'datetime_immutable', + 'string', + 'timestamp', + 'array', + ]); + $resolver->setAllowedValues('widget', [ + 'single_text', + 'text', + 'choice', + ]); + + $resolver->setAllowedTypes('format', ['int', 'string']); + $resolver->setAllowedTypes('years', 'array'); + $resolver->setAllowedTypes('months', 'array'); + $resolver->setAllowedTypes('days', 'array'); + $resolver->setAllowedTypes('input_format', 'string'); + + $resolver->setNormalizer('html5', static function (Options $options, $html5) { + if ($html5 && 'single_text' === $options['widget'] && self::HTML5_FORMAT !== $options['format']) { + throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class)); + } + + return $html5; + }); + } + + public function getBlockPrefix(): string + { + return 'date'; + } + + private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, array $timestamps): array + { + $pattern = $formatter->getPattern(); + $timezone = $formatter->getTimeZoneId(); + $formattedTimestamps = []; + + $formatter->setTimeZone('UTC'); + + if (preg_match($regex, $pattern, $matches)) { + $formatter->setPattern($matches[0]); + + foreach ($timestamps as $timestamp => $choice) { + $formattedTimestamps[$formatter->format($timestamp)] = $choice; + } + + // I'd like to clone the formatter above, but then we get a + // segmentation fault, so let's restore the old state instead + $formatter->setPattern($pattern); + } + + $formatter->setTimeZone($timezone); + + return $formattedTimestamps; + } + + private function listYears(array $years): array + { + $result = []; + + foreach ($years as $year) { + $result[\PHP_INT_SIZE === 4 ? \DateTimeImmutable::createFromFormat('Y e', $year.' UTC')->format('U') : gmmktime(0, 0, 0, 6, 15, $year)] = $year; + } + + return $result; + } + + private function listMonths(array $months): array + { + $result = []; + + foreach ($months as $month) { + $result[gmmktime(0, 0, 0, $month, 15)] = $month; + } + + return $result; + } + + private function listDays(array $days): array + { + $result = []; + + foreach ($days as $day) { + $result[gmmktime(0, 0, 0, 5, $day)] = $day; + } + + return $result; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/EmailType.php b/lib/symfony/form/Extension/Core/Type/EmailType.php new file mode 100644 index 0000000000..64d01ee67a --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/EmailType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class EmailType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'invalid_message' => 'Please enter a valid email address.', + ]); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'email'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/EnumType.php b/lib/symfony/form/Extension/Core/Type/EnumType.php new file mode 100644 index 0000000000..bfede9c04d --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/EnumType.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatableInterface; + +/** + * A choice type for native PHP enums. + * + * @author Alexander M. Turek + */ +final class EnumType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setRequired(['class']) + ->setAllowedTypes('class', 'string') + ->setAllowedValues('class', enum_exists(...)) + ->setDefault('choices', static fn (Options $options): array => $options['class']::cases()) + ->setDefault('choice_label', static fn (\UnitEnum $choice) => $choice instanceof TranslatableInterface ? $choice : $choice->name) + ->setDefault('choice_value', static function (Options $options): ?\Closure { + if (!is_a($options['class'], \BackedEnum::class, true)) { + return null; + } + + return static function (?\BackedEnum $choice): ?string { + if (null === $choice) { + return null; + } + + return (string) $choice->value; + }; + }) + ; + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/FileType.php b/lib/symfony/form/Extension/Core/Type/FileType.php new file mode 100644 index 0000000000..bbf01a80af --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/FileType.php @@ -0,0 +1,243 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Event\PreSubmitEvent; +use Symfony\Component\Form\FileUploadError; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +class FileType extends AbstractType +{ + public const KIB_BYTES = 1024; + public const MIB_BYTES = 1048576; + + private const SUFFIXES = [ + 1 => 'bytes', + self::KIB_BYTES => 'KiB', + self::MIB_BYTES => 'MiB', + ]; + + private ?TranslatorInterface $translator; + + public function __construct(?TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + // Ensure that submitted data is always an uploaded file or an array of some + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { + /** @var PreSubmitEvent $event */ + $form = $event->getForm(); + $requestHandler = $form->getConfig()->getRequestHandler(); + + if ($options['multiple']) { + $data = []; + $files = $event->getData(); + + if (!\is_array($files)) { + $files = []; + } + + foreach ($files as $file) { + if ($requestHandler->isFileUpload($file)) { + $data[] = $file; + + if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) { + $form->addError($this->getFileUploadError($errorCode)); + } + } + } + + // Since the array is never considered empty in the view data format + // on submission, we need to evaluate the configured empty data here + if ([] === $data) { + $emptyData = $form->getConfig()->getEmptyData(); + $data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData; + } + + $event->setData($data); + } elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) { + $form->addError($this->getFileUploadError($errorCode)); + } elseif (!$requestHandler->isFileUpload($event->getData())) { + $event->setData(null); + } + }); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + if ($options['multiple']) { + $view->vars['full_name'] .= '[]'; + $view->vars['attr']['multiple'] = 'multiple'; + } + + $view->vars = array_replace($view->vars, [ + 'type' => 'file', + 'value' => '', + ]); + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $view->vars['multipart'] = true; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $dataClass = null; + if (class_exists(File::class)) { + $dataClass = static fn (Options $options) => $options['multiple'] ? null : File::class; + } + + $emptyData = static fn (Options $options) => $options['multiple'] ? [] : null; + + $resolver->setDefaults([ + 'compound' => false, + 'data_class' => $dataClass, + 'empty_data' => $emptyData, + 'multiple' => false, + 'allow_file_upload' => true, + 'invalid_message' => 'Please select a valid file.', + ]); + } + + public function getBlockPrefix(): string + { + return 'file'; + } + + private function getFileUploadError(int $errorCode): FileUploadError + { + $messageParameters = []; + + if (\UPLOAD_ERR_INI_SIZE === $errorCode) { + [$limitAsString, $suffix] = $this->factorizeSizes(0, self::getMaxFilesize()); + $messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'; + $messageParameters = [ + '{{ limit }}' => $limitAsString, + '{{ suffix }}' => $suffix, + ]; + } elseif (\UPLOAD_ERR_FORM_SIZE === $errorCode) { + $messageTemplate = 'The file is too large.'; + } else { + $messageTemplate = 'The file could not be uploaded.'; + } + + if (null !== $this->translator) { + $message = $this->translator->trans($messageTemplate, $messageParameters, 'validators'); + } else { + $message = strtr($messageTemplate, $messageParameters); + } + + return new FileUploadError($message, $messageTemplate, $messageParameters); + } + + /** + * Returns the maximum size of an uploaded file as configured in php.ini. + * + * This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize(). + */ + private static function getMaxFilesize(): int|float + { + $iniMax = strtolower(\ini_get('upload_max_filesize')); + + if ('' === $iniMax) { + return \PHP_INT_MAX; + } + + $max = ltrim($iniMax, '+'); + if (str_starts_with($max, '0x')) { + $max = \intval($max, 16); + } elseif (str_starts_with($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($iniMax, -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + + return $max; + } + + /** + * Converts the limit to the smallest possible number + * (i.e. try "MB", then "kB", then "bytes"). + * + * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes(). + */ + private function factorizeSizes(int $size, int|float $limit): array + { + $coef = self::MIB_BYTES; + $coefFactor = self::KIB_BYTES; + + $limitAsString = (string) ($limit / $coef); + + // Restrict the limit to 2 decimals (without rounding! we + // need the precise value) + while (self::moreDecimalsThan($limitAsString, 2)) { + $coef /= $coefFactor; + $limitAsString = (string) ($limit / $coef); + } + + // Convert size to the same measure, but round to 2 decimals + $sizeAsString = (string) round($size / $coef, 2); + + // If the size and limit produce the same string output + // (due to rounding), reduce the coefficient + while ($sizeAsString === $limitAsString) { + $coef /= $coefFactor; + $limitAsString = (string) ($limit / $coef); + $sizeAsString = (string) round($size / $coef, 2); + } + + return [$limitAsString, self::SUFFIXES[$coef]]; + } + + /** + * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan(). + */ + private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool + { + return \strlen($double) > \strlen(round($double, $numberOfDecimals)); + } +} diff --git a/lib/symfony/form/Extension/Core/Type/FormType.php b/lib/symfony/form/Extension/Core/Type/FormType.php new file mode 100644 index 0000000000..432ba78cd7 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/FormType.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor; +use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor; +use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; +use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; +use Symfony\Component\Form\Extension\Core\EventListener\TrimListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Translation\TranslatableInterface; + +class FormType extends BaseType +{ + private DataMapper $dataMapper; + + public function __construct(?PropertyAccessorInterface $propertyAccessor = null) + { + $this->dataMapper = new DataMapper(new ChainAccessor([ + new CallbackAccessor(), + new PropertyPathAccessor($propertyAccessor ?? PropertyAccess::createPropertyAccessor()), + ])); + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + parent::buildForm($builder, $options); + + $isDataOptionSet = \array_key_exists('data', $options); + + $builder + ->setRequired($options['required']) + ->setErrorBubbling($options['error_bubbling']) + ->setEmptyData($options['empty_data']) + ->setPropertyPath($options['property_path']) + ->setMapped($options['mapped']) + ->setByReference($options['by_reference']) + ->setInheritData($options['inherit_data']) + ->setCompound($options['compound']) + ->setData($isDataOptionSet ? $options['data'] : null) + ->setDataLocked($isDataOptionSet) + ->setDataMapper($options['compound'] ? $this->dataMapper : null) + ->setMethod($options['method']) + ->setAction($options['action']); + + if ($options['trim']) { + $builder->addEventSubscriber(new TrimListener()); + } + + $builder->setIsEmptyCallback($options['is_empty_callback']); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + + $name = $form->getName(); + $helpTranslationParameters = $options['help_translation_parameters']; + + if ($view->parent) { + if ('' === $name) { + throw new LogicException('Form node with empty name can be used only as root form node.'); + } + + // Complex fields are read-only if they themselves or their parents are. + if (!isset($view->vars['attr']['readonly']) && isset($view->parent->vars['attr']['readonly']) && false !== $view->parent->vars['attr']['readonly']) { + $view->vars['attr']['readonly'] = true; + } + + $helpTranslationParameters = array_merge($view->parent->vars['help_translation_parameters'], $helpTranslationParameters); + } + + $formConfig = $form->getConfig(); + $view->vars = array_replace($view->vars, [ + 'errors' => $form->getErrors(), + 'valid' => $form->isSubmitted() ? $form->isValid() : true, + 'value' => $form->getViewData(), + 'data' => $form->getNormData(), + 'required' => $form->isRequired(), + 'label_attr' => $options['label_attr'], + 'help' => $options['help'], + 'help_attr' => $options['help_attr'], + 'help_html' => $options['help_html'], + 'help_translation_parameters' => $helpTranslationParameters, + 'compound' => $formConfig->getCompound(), + 'method' => $formConfig->getMethod(), + 'action' => $formConfig->getAction(), + 'submitted' => $form->isSubmitted(), + ]); + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $multipart = false; + + foreach ($view->children as $child) { + if ($child->vars['multipart']) { + $multipart = true; + break; + } + } + + $view->vars['multipart'] = $multipart; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + // Derive "data_class" option from passed "data" object + $dataClass = static fn (Options $options) => isset($options['data']) && \is_object($options['data']) ? $options['data']::class : null; + + // Derive "empty_data" closure from "data_class" option + $emptyData = static function (Options $options) { + $class = $options['data_class']; + + if (null !== $class) { + return static fn (FormInterface $form) => $form->isEmpty() && !$form->isRequired() ? null : new $class(); + } + + return static fn (FormInterface $form) => $form->getConfig()->getCompound() ? [] : ''; + }; + + // Wrap "post_max_size_message" in a closure to translate it lazily + $uploadMaxSizeMessage = static fn (Options $options) => static fn () => $options['post_max_size_message']; + + // For any form that is not represented by a single HTML control, + // errors should bubble up by default + $errorBubbling = static fn (Options $options) => $options['compound'] && !$options['inherit_data']; + + // If data is given, the form is locked to that data + // (independent of its value) + $resolver->setDefined([ + 'data', + ]); + + $resolver->setDefaults([ + 'data_class' => $dataClass, + 'empty_data' => $emptyData, + 'trim' => true, + 'required' => true, + 'property_path' => null, + 'mapped' => true, + 'by_reference' => true, + 'error_bubbling' => $errorBubbling, + 'label_attr' => [], + 'inherit_data' => false, + 'compound' => true, + 'method' => 'POST', + // According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt) + // section 4.2., empty URIs are considered same-document references + 'action' => '', + 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', + 'upload_max_size_message' => $uploadMaxSizeMessage, // internal + 'allow_file_upload' => false, + 'help' => null, + 'help_attr' => [], + 'help_html' => false, + 'help_translation_parameters' => [], + 'invalid_message' => 'This value is not valid.', + 'invalid_message_parameters' => [], + 'is_empty_callback' => null, + 'getter' => null, + 'setter' => null, + ]); + + $resolver->setAllowedTypes('label_attr', 'array'); + $resolver->setAllowedTypes('action', 'string'); + $resolver->setAllowedTypes('upload_max_size_message', ['callable']); + $resolver->setAllowedTypes('help', ['string', 'null', TranslatableInterface::class]); + $resolver->setAllowedTypes('help_attr', 'array'); + $resolver->setAllowedTypes('help_html', 'bool'); + $resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']); + $resolver->setAllowedTypes('getter', ['null', 'callable']); + $resolver->setAllowedTypes('setter', ['null', 'callable']); + + $resolver->setInfo('getter', 'A callable that accepts two arguments (the view data and the current form field) and must return a value.'); + $resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).'); + } + + public function getParent(): ?string + { + return null; + } + + public function getBlockPrefix(): string + { + return 'form'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/HiddenType.php b/lib/symfony/form/Extension/Core/Type/HiddenType.php new file mode 100644 index 0000000000..c4e5eb2ccf --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/HiddenType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class HiddenType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + // hidden fields cannot have a required attribute + 'required' => false, + // Pass errors to the parent + 'error_bubbling' => true, + 'compound' => false, + 'invalid_message' => 'The hidden field is invalid.', + ]); + } + + public function getBlockPrefix(): string + { + return 'hidden'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/IntegerType.php b/lib/symfony/form/Extension/Core/Type/IntegerType.php new file mode 100644 index 0000000000..a287b66b7c --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/IntegerType.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class IntegerType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'], !$options['grouping'] ? 'en' : null)); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + if ($options['grouping']) { + $view->vars['type'] = 'text'; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'grouping' => false, + // Integer cast rounds towards 0, so do the same when displaying fractions + 'rounding_mode' => \NumberFormatter::ROUND_DOWN, + 'compound' => false, + 'invalid_message' => 'Please enter an integer.', + ]); + + $resolver->setAllowedValues('rounding_mode', [ + \NumberFormatter::ROUND_FLOOR, + \NumberFormatter::ROUND_DOWN, + \NumberFormatter::ROUND_HALFDOWN, + \NumberFormatter::ROUND_HALFEVEN, + \NumberFormatter::ROUND_HALFUP, + \NumberFormatter::ROUND_UP, + \NumberFormatter::ROUND_CEILING, + ]); + } + + public function getBlockPrefix(): string + { + return 'integer'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/LanguageType.php b/lib/symfony/form/Extension/Core/Type/LanguageType.php new file mode 100644 index 0000000000..e1a7c1bf30 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/LanguageType.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\Languages; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class LanguageType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'choice_loader' => function (Options $options) { + if (!class_exists(Intl::class)) { + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + } + $choiceTranslationLocale = $options['choice_translation_locale']; + $useAlpha3Codes = $options['alpha3']; + $choiceSelfTranslation = $options['choice_self_translation']; + + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) { + if (true === $choiceSelfTranslation) { + foreach (Languages::getLanguageCodes() as $alpha2Code) { + try { + $languageCode = $useAlpha3Codes ? Languages::getAlpha3Code($alpha2Code) : $alpha2Code; + $languagesList[$languageCode] = Languages::getName($alpha2Code, $alpha2Code); + } catch (MissingResourceException) { + // ignore errors like "Couldn't read the indices for the locale 'meta'" + } + } + } else { + $languagesList = $useAlpha3Codes ? Languages::getAlpha3Names($choiceTranslationLocale) : Languages::getNames($choiceTranslationLocale); + } + + return array_flip($languagesList); + }), [$choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation]); + }, + 'choice_translation_domain' => false, + 'choice_translation_locale' => null, + 'alpha3' => false, + 'choice_self_translation' => false, + 'invalid_message' => 'Please select a valid language.', + ]); + + $resolver->setAllowedTypes('choice_self_translation', ['bool']); + $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); + $resolver->setAllowedTypes('alpha3', 'bool'); + + $resolver->setNormalizer('choice_self_translation', static function (Options $options, $value) { + if (true === $value && $options['choice_translation_locale']) { + throw new LogicException('Cannot use the "choice_self_translation" and "choice_translation_locale" options at the same time. Remove one of them.'); + } + + return $value; + }); + } + + public function getParent(): ?string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'language'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/LocaleType.php b/lib/symfony/form/Extension/Core/Type/LocaleType.php new file mode 100644 index 0000000000..1ea378f453 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/LocaleType.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\Locales; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class LocaleType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'choice_loader' => function (Options $options) { + if (!class_exists(Intl::class)) { + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + } + + $choiceTranslationLocale = $options['choice_translation_locale']; + + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Locales::getNames($choiceTranslationLocale))), $choiceTranslationLocale); + }, + 'choice_translation_domain' => false, + 'choice_translation_locale' => null, + 'invalid_message' => 'Please select a valid locale.', + ]); + + $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); + } + + public function getParent(): ?string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'locale'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/MoneyType.php b/lib/symfony/form/Extension/Core/Type/MoneyType.php new file mode 100644 index 0000000000..9c9e5b4d7c --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/MoneyType.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class MoneyType extends AbstractType +{ + protected static $patterns = []; + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping, + // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats + $builder + ->addViewTransformer(new MoneyToLocalizedStringTransformer( + $options['scale'], + $options['grouping'], + $options['rounding_mode'], + $options['divisor'], + $options['html5'] ? 'en' : null + )) + ; + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['money_pattern'] = self::getPattern($options['currency']); + + if ($options['html5']) { + $view->vars['type'] = 'number'; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'scale' => 2, + 'grouping' => false, + 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, + 'divisor' => 1, + 'currency' => 'EUR', + 'compound' => false, + 'html5' => false, + 'invalid_message' => 'Please enter a valid money amount.', + ]); + + $resolver->setAllowedValues('rounding_mode', [ + \NumberFormatter::ROUND_FLOOR, + \NumberFormatter::ROUND_DOWN, + \NumberFormatter::ROUND_HALFDOWN, + \NumberFormatter::ROUND_HALFEVEN, + \NumberFormatter::ROUND_HALFUP, + \NumberFormatter::ROUND_UP, + \NumberFormatter::ROUND_CEILING, + ]); + + $resolver->setAllowedTypes('scale', 'int'); + + $resolver->setAllowedTypes('html5', 'bool'); + + $resolver->setNormalizer('grouping', static function (Options $options, $value) { + if ($value && $options['html5']) { + throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.'); + } + + return $value; + }); + } + + public function getBlockPrefix(): string + { + return 'money'; + } + + /** + * Returns the pattern for this locale in UTF-8. + * + * The pattern contains the placeholder "{{ widget }}" where the HTML tag should + * be inserted + * + * @return string + */ + protected static function getPattern(?string $currency) + { + if (!$currency) { + return '{{ widget }}'; + } + + $locale = \Locale::getDefault(); + + if (!isset(self::$patterns[$locale])) { + self::$patterns[$locale] = []; + } + + if (!isset(self::$patterns[$locale][$currency])) { + $format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); + $pattern = $format->formatCurrency('123', $currency); + + // the spacings between currency symbol and number are ignored, because + // a single space leads to better readability in combination with input + // fields + + // the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8) + + preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123(?:[,.]0+)?[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/u', $pattern, $matches); + + if (!empty($matches[1])) { + self::$patterns[$locale][$currency] = $matches[1].' {{ widget }}'; + } elseif (!empty($matches[2])) { + self::$patterns[$locale][$currency] = '{{ widget }} '.$matches[2]; + } else { + self::$patterns[$locale][$currency] = '{{ widget }}'; + } + } + + return self::$patterns[$locale][$currency]; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/NumberType.php b/lib/symfony/form/Extension/Core/Type/NumberType.php new file mode 100644 index 0000000000..578991f9fd --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/NumberType.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class NumberType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addViewTransformer(new NumberToLocalizedStringTransformer( + $options['scale'], + $options['grouping'], + $options['rounding_mode'], + $options['html5'] ? 'en' : null + )); + + if ('string' === $options['input']) { + $builder->addModelTransformer(new StringToFloatTransformer($options['scale'])); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + if ($options['html5']) { + $view->vars['type'] = 'number'; + + if (!isset($view->vars['attr']['step'])) { + $view->vars['attr']['step'] = 'any'; + } + } else { + $view->vars['attr']['inputmode'] = 0 === $options['scale'] ? 'numeric' : 'decimal'; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + // default scale is locale specific (usually around 3) + 'scale' => null, + 'grouping' => false, + 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, + 'compound' => false, + 'input' => 'number', + 'html5' => false, + 'invalid_message' => 'Please enter a number.', + ]); + + $resolver->setAllowedValues('rounding_mode', [ + \NumberFormatter::ROUND_FLOOR, + \NumberFormatter::ROUND_DOWN, + \NumberFormatter::ROUND_HALFDOWN, + \NumberFormatter::ROUND_HALFEVEN, + \NumberFormatter::ROUND_HALFUP, + \NumberFormatter::ROUND_UP, + \NumberFormatter::ROUND_CEILING, + ]); + $resolver->setAllowedValues('input', ['number', 'string']); + $resolver->setAllowedTypes('scale', ['null', 'int']); + $resolver->setAllowedTypes('html5', 'bool'); + + $resolver->setNormalizer('grouping', static function (Options $options, $value) { + if (true === $value && $options['html5']) { + throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.'); + } + + return $value; + }); + } + + public function getBlockPrefix(): string + { + return 'number'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/PasswordType.php b/lib/symfony/form/Extension/Core/Type/PasswordType.php new file mode 100644 index 0000000000..0c247f0f30 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/PasswordType.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class PasswordType extends AbstractType +{ + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + if ($options['always_empty'] || !$form->isSubmitted()) { + $view->vars['value'] = ''; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'always_empty' => true, + 'trim' => false, + 'invalid_message' => 'The password is invalid.', + ]); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'password'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/PercentType.php b/lib/symfony/form/Extension/Core/Type/PercentType.php new file mode 100644 index 0000000000..f71e288b3e --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/PercentType.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class PercentType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addViewTransformer(new PercentToLocalizedStringTransformer( + $options['scale'], + $options['type'], + $options['rounding_mode'], + $options['html5'] + )); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['symbol'] = $options['symbol']; + + if ($options['html5']) { + $view->vars['type'] = 'number'; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'scale' => 0, + 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, + 'symbol' => '%', + 'type' => 'fractional', + 'compound' => false, + 'html5' => false, + 'invalid_message' => 'Please enter a percentage value.', + ]); + + $resolver->setAllowedValues('type', [ + 'fractional', + 'integer', + ]); + $resolver->setAllowedValues('rounding_mode', [ + \NumberFormatter::ROUND_FLOOR, + \NumberFormatter::ROUND_DOWN, + \NumberFormatter::ROUND_HALFDOWN, + \NumberFormatter::ROUND_HALFEVEN, + \NumberFormatter::ROUND_HALFUP, + \NumberFormatter::ROUND_UP, + \NumberFormatter::ROUND_CEILING, + ]); + $resolver->setAllowedTypes('scale', 'int'); + $resolver->setAllowedTypes('symbol', ['bool', 'string']); + $resolver->setAllowedTypes('html5', 'bool'); + } + + public function getBlockPrefix(): string + { + return 'percent'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/RadioType.php b/lib/symfony/form/Extension/Core/Type/RadioType.php new file mode 100644 index 0000000000..4b97b0ae21 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/RadioType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class RadioType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'invalid_message' => 'Please select a valid option.', + ]); + } + + public function getParent(): ?string + { + return CheckboxType::class; + } + + public function getBlockPrefix(): string + { + return 'radio'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/RangeType.php b/lib/symfony/form/Extension/Core/Type/RangeType.php new file mode 100644 index 0000000000..2e33a977d9 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/RangeType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class RangeType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'invalid_message' => 'Please choose a valid range.', + ]); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'range'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/RepeatedType.php b/lib/symfony/form/Extension/Core/Type/RepeatedType.php new file mode 100644 index 0000000000..4176f93e52 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/RepeatedType.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class RepeatedType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + // Overwrite required option for child fields + $options['first_options']['required'] = $options['required']; + $options['second_options']['required'] = $options['required']; + + if (!isset($options['options']['error_bubbling'])) { + $options['options']['error_bubbling'] = $options['error_bubbling']; + } + + // children fields must always be mapped + $defaultOptions = ['mapped' => true]; + + $builder + ->addViewTransformer(new ValueToDuplicatesTransformer([ + $options['first_name'], + $options['second_name'], + ])) + ->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options'], $defaultOptions)) + ->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options'], $defaultOptions)) + ; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'type' => TextType::class, + 'options' => [], + 'first_options' => [], + 'second_options' => [], + 'first_name' => 'first', + 'second_name' => 'second', + 'error_bubbling' => false, + 'invalid_message' => 'The values do not match.', + ]); + + $resolver->setAllowedTypes('options', 'array'); + $resolver->setAllowedTypes('first_options', 'array'); + $resolver->setAllowedTypes('second_options', 'array'); + } + + public function getBlockPrefix(): string + { + return 'repeated'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/ResetType.php b/lib/symfony/form/Extension/Core/Type/ResetType.php new file mode 100644 index 0000000000..9a53a3dc68 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/ResetType.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ButtonTypeInterface; + +/** + * A reset button. + * + * @author Bernhard Schussek + */ +class ResetType extends AbstractType implements ButtonTypeInterface +{ + public function getParent(): ?string + { + return ButtonType::class; + } + + public function getBlockPrefix(): string + { + return 'reset'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/SearchType.php b/lib/symfony/form/Extension/Core/Type/SearchType.php new file mode 100644 index 0000000000..0dca6e42a8 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/SearchType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class SearchType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'invalid_message' => 'Please enter a valid search term.', + ]); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'search'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/SubmitType.php b/lib/symfony/form/Extension/Core/Type/SubmitType.php new file mode 100644 index 0000000000..3f1b5f95c9 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/SubmitType.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\SubmitButtonTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * A submit button. + * + * @author Bernhard Schussek + */ +class SubmitType extends AbstractType implements SubmitButtonTypeInterface +{ + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['clicked'] = $form->isClicked(); + + if (!$options['validate']) { + $view->vars['attr']['formnovalidate'] = true; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('validate', true); + $resolver->setAllowedTypes('validate', 'bool'); + } + + public function getParent(): ?string + { + return ButtonType::class; + } + + public function getBlockPrefix(): string + { + return 'submit'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/TelType.php b/lib/symfony/form/Extension/Core/Type/TelType.php new file mode 100644 index 0000000000..05fdd41626 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/TelType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class TelType extends AbstractType +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'invalid_message' => 'Please provide a valid phone number.', + ]); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'tel'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/TextType.php b/lib/symfony/form/Extension/Core/Type/TextType.php new file mode 100644 index 0000000000..479ce054d8 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/TextType.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class TextType extends AbstractType implements DataTransformerInterface +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + // When empty_data is explicitly set to an empty string, + // a string should always be returned when NULL is submitted + // This gives more control and thus helps preventing some issues + // with PHP 7 which allows type hinting strings in functions + // See https://github.com/symfony/symfony/issues/5906#issuecomment-203189375 + if ('' === $options['empty_data']) { + $builder->addViewTransformer($this); + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'compound' => false, + ]); + } + + public function getBlockPrefix(): string + { + return 'text'; + } + + public function transform(mixed $data): mixed + { + // Model data should not be transformed + return $data; + } + + public function reverseTransform(mixed $data): mixed + { + return $data ?? ''; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/TextareaType.php b/lib/symfony/form/Extension/Core/Type/TextareaType.php new file mode 100644 index 0000000000..40e7580d80 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/TextareaType.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; + +class TextareaType extends AbstractType +{ + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['pattern'] = null; + unset($view->vars['attr']['pattern']); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'textarea'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/TimeType.php b/lib/symfony/form/Extension/Core/Type/TimeType.php new file mode 100644 index 0000000000..ad697634ca --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/TimeType.php @@ -0,0 +1,398 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Event\PreSubmitEvent; +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class TimeType extends AbstractType +{ + private const WIDGETS = [ + 'text' => TextType::class, + 'choice' => ChoiceType::class, + ]; + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $parts = ['hour']; + $format = 'H'; + + if ($options['with_seconds'] && !$options['with_minutes']) { + throw new InvalidConfigurationException('You cannot disable minutes if you have enabled seconds.'); + } + + if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) { + throw new InvalidConfigurationException(\sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).', $options['model_timezone'], $options['reference_date']->getTimezone()->getName())); + } + + if ($options['with_minutes']) { + $format .= ':i'; + $parts[] = 'minute'; + } + + if ($options['with_seconds']) { + $format .= ':s'; + $parts[] = 'second'; + } + + if ('single_text' === $options['widget']) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $e) use ($options) { + /** @var PreSubmitEvent $event */ + $data = $e->getData(); + if ($data && preg_match('/^(?P\d{2}):(?P\d{2})(?::(?P\d{2})(?:\.\d+)?)?$/', $data, $matches)) { + if ($options['with_seconds']) { + // handle seconds ignored by user's browser when with_seconds enabled + // https://codereview.chromium.org/450533009/ + $e->setData(\sprintf('%s:%s:%s', $matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00')); + } else { + $e->setData(\sprintf('%s:%s', $matches['hours'], $matches['minutes'])); + } + } + }); + + $parseFormat = null; + + if (null !== $options['reference_date']) { + $parseFormat = 'Y-m-d '.$format; + + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($options) { + $data = $event->getData(); + + if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) { + $event->setData($options['reference_date']->format('Y-m-d ').$data); + } + }); + } + + $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format, $parseFormat)); + } else { + $hourOptions = $minuteOptions = $secondOptions = [ + 'error_bubbling' => true, + 'empty_data' => '', + ]; + // when the form is compound the entries of the array are ignored in favor of children data + // so we need to handle the cascade setting here + $emptyData = $builder->getEmptyData() ?: []; + + if ($emptyData instanceof \Closure) { + $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) { + $emptyData = $emptyData($form->getParent()); + + return $emptyData[$option] ?? ''; + }; + + $hourOptions['empty_data'] = $lazyEmptyData('hour'); + } elseif (isset($emptyData['hour'])) { + $hourOptions['empty_data'] = $emptyData['hour']; + } + + if (isset($options['invalid_message'])) { + $hourOptions['invalid_message'] = $options['invalid_message']; + $minuteOptions['invalid_message'] = $options['invalid_message']; + $secondOptions['invalid_message'] = $options['invalid_message']; + } + + if (isset($options['invalid_message_parameters'])) { + $hourOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + $minuteOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + $secondOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + } + + if ('choice' === $options['widget']) { + $hours = $minutes = []; + + foreach ($options['hours'] as $hour) { + $hours[str_pad($hour, 2, '0', \STR_PAD_LEFT)] = $hour; + } + + // Only pass a subset of the options to children + $hourOptions['choices'] = $hours; + $hourOptions['placeholder'] = $options['placeholder']['hour']; + $hourOptions['choice_translation_domain'] = $options['choice_translation_domain']['hour']; + + if ($options['with_minutes']) { + foreach ($options['minutes'] as $minute) { + $minutes[str_pad($minute, 2, '0', \STR_PAD_LEFT)] = $minute; + } + + $minuteOptions['choices'] = $minutes; + $minuteOptions['placeholder'] = $options['placeholder']['minute']; + $minuteOptions['choice_translation_domain'] = $options['choice_translation_domain']['minute']; + } + + if ($options['with_seconds']) { + $seconds = []; + + foreach ($options['seconds'] as $second) { + $seconds[str_pad($second, 2, '0', \STR_PAD_LEFT)] = $second; + } + + $secondOptions['choices'] = $seconds; + $secondOptions['placeholder'] = $options['placeholder']['second']; + $secondOptions['choice_translation_domain'] = $options['choice_translation_domain']['second']; + } + + // Append generic carry-along options + foreach (['required', 'translation_domain'] as $passOpt) { + $hourOptions[$passOpt] = $options[$passOpt]; + + if ($options['with_minutes']) { + $minuteOptions[$passOpt] = $options[$passOpt]; + } + + if ($options['with_seconds']) { + $secondOptions[$passOpt] = $options[$passOpt]; + } + } + } + + $builder->add('hour', self::WIDGETS[$options['widget']], $hourOptions); + + if ($options['with_minutes']) { + if ($emptyData instanceof \Closure) { + $minuteOptions['empty_data'] = $lazyEmptyData('minute'); + } elseif (isset($emptyData['minute'])) { + $minuteOptions['empty_data'] = $emptyData['minute']; + } + $builder->add('minute', self::WIDGETS[$options['widget']], $minuteOptions); + } + + if ($options['with_seconds']) { + if ($emptyData instanceof \Closure) { + $secondOptions['empty_data'] = $lazyEmptyData('second'); + } elseif (isset($emptyData['second'])) { + $secondOptions['empty_data'] = $emptyData['second']; + } + $builder->add('second', self::WIDGETS[$options['widget']], $secondOptions); + } + + $builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date'])); + } + + if ('datetime_immutable' === $options['input']) { + $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer()); + } elseif ('string' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format']) + )); + } elseif ('timestamp' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone']) + )); + } elseif ('array' === $options['input']) { + $builder->addModelTransformer(new ReversedTransformer( + new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']) + )); + } + + if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $date = $event->getData(); + + if (!$date instanceof \DateTimeInterface) { + return; + } + + if ($date->getTimezone()->getName() !== $options['model_timezone']) { + trigger_deprecation('symfony/form', '6.4', \sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is deprecated.', $date::class, $date->getTimezone()->getName(), $options['model_timezone'])); + // throw new LogicException(sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', $date::class, $date->getTimezone()->getName(), $options['model_timezone'])); + } + }); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars = array_replace($view->vars, [ + 'widget' => $options['widget'], + 'with_minutes' => $options['with_minutes'], + 'with_seconds' => $options['with_seconds'], + ]); + + // Change the input to an HTML5 time input if + // * the widget is set to "single_text" + // * the html5 is set to true + if ($options['html5'] && 'single_text' === $options['widget']) { + $view->vars['type'] = 'time'; + + // we need to force the browser to display the seconds by + // adding the HTML attribute step if not already defined. + // Otherwise the browser will not display and so not send the seconds + // therefore the value will always be considered as invalid. + if (!isset($view->vars['attr']['step'])) { + if ($options['with_seconds']) { + $view->vars['attr']['step'] = 1; + } elseif (!$options['with_minutes']) { + $view->vars['attr']['step'] = 3600; + } + } + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; + + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; + + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { + if (\is_array($placeholder)) { + $default = $placeholderDefault($options); + + return array_merge( + ['hour' => $default, 'minute' => $default, 'second' => $default], + $placeholder + ); + } + + return [ + 'hour' => $placeholder, + 'minute' => $placeholder, + 'second' => $placeholder, + ]; + }; + + $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { + if (\is_array($choiceTranslationDomain)) { + $default = false; + + return array_replace( + ['hour' => $default, 'minute' => $default, 'second' => $default], + $choiceTranslationDomain + ); + } + + return [ + 'hour' => $choiceTranslationDomain, + 'minute' => $choiceTranslationDomain, + 'second' => $choiceTranslationDomain, + ]; + }; + + $modelTimezone = static function (Options $options, $value): ?string { + if (null !== $value) { + return $value; + } + + if (null !== $options['reference_date']) { + return $options['reference_date']->getTimezone()->getName(); + } + + return null; + }; + + $viewTimezone = static function (Options $options, $value): ?string { + if (null !== $value) { + return $value; + } + + if (null !== $options['model_timezone'] && null === $options['reference_date']) { + return $options['model_timezone']; + } + + return null; + }; + + $resolver->setDefaults([ + 'hours' => range(0, 23), + 'minutes' => range(0, 59), + 'seconds' => range(0, 59), + 'widget' => static function (Options $options) { + trigger_deprecation('symfony/form', '6.3', 'Not configuring the "widget" option of form type "time" is deprecated. It will default to "single_text" in Symfony 7.0.'); + + return 'choice'; + }, + 'input' => 'datetime', + 'input_format' => 'H:i:s', + 'with_minutes' => true, + 'with_seconds' => false, + 'model_timezone' => $modelTimezone, + 'view_timezone' => $viewTimezone, + 'reference_date' => null, + 'placeholder' => $placeholderDefault, + 'html5' => true, + // Don't modify \DateTime classes by reference, we treat + // them like immutable value objects + 'by_reference' => false, + 'error_bubbling' => false, + // If initialized with a \DateTime object, FormType initializes + // this option to "\DateTime". Since the internal, normalized + // representation is not \DateTime, but an array, we need to unset + // this option. + 'data_class' => null, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', + 'compound' => $compound, + 'choice_translation_domain' => false, + 'invalid_message' => 'Please enter a valid time.', + ]); + + $resolver->setNormalizer('view_timezone', static function (Options $options, $viewTimezone): ?string { + if (null !== $options['model_timezone'] && $viewTimezone !== $options['model_timezone'] && null === $options['reference_date']) { + throw new LogicException('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is not supported.'); + } + + return $viewTimezone; + }); + + $resolver->setNormalizer('placeholder', $placeholderNormalizer); + $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); + + $resolver->setAllowedValues('input', [ + 'datetime', + 'datetime_immutable', + 'string', + 'timestamp', + 'array', + ]); + $resolver->setAllowedValues('widget', [ + 'single_text', + 'text', + 'choice', + ]); + + $resolver->setAllowedTypes('hours', 'array'); + $resolver->setAllowedTypes('minutes', 'array'); + $resolver->setAllowedTypes('seconds', 'array'); + $resolver->setAllowedTypes('input_format', 'string'); + $resolver->setAllowedTypes('model_timezone', ['null', 'string']); + $resolver->setAllowedTypes('view_timezone', ['null', 'string']); + $resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]); + } + + public function getBlockPrefix(): string + { + return 'time'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/TimezoneType.php b/lib/symfony/form/Extension/Core/Type/TimezoneType.php new file mode 100644 index 0000000000..af11c11df4 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/TimezoneType.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\Timezones; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class TimezoneType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ('datetimezone' === $options['input']) { + $builder->addModelTransformer(new DateTimeZoneToStringTransformer($options['multiple'])); + } elseif ('intltimezone' === $options['input']) { + $builder->addModelTransformer(new IntlTimeZoneToStringTransformer($options['multiple'])); + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'intl' => false, + 'choice_loader' => function (Options $options) { + $input = $options['input']; + + if ($options['intl']) { + if (!class_exists(Intl::class)) { + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s" with option "intl=true". Try running "composer require symfony/intl".', static::class)); + } + + $choiceTranslationLocale = $options['choice_translation_locale']; + + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => self::getIntlTimezones($input, $choiceTranslationLocale)), [$input, $choiceTranslationLocale]); + } + + return ChoiceList::lazy($this, static fn () => self::getPhpTimezones($input), $input); + }, + 'choice_translation_domain' => false, + 'choice_translation_locale' => null, + 'input' => 'string', + 'invalid_message' => 'Please select a valid timezone.', + 'regions' => \DateTimeZone::ALL, + ]); + + $resolver->setAllowedTypes('intl', ['bool']); + + $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); + $resolver->setNormalizer('choice_translation_locale', static function (Options $options, $value) { + if (null !== $value && !$options['intl']) { + throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.'); + } + + return $value; + }); + + $resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']); + $resolver->setNormalizer('input', static function (Options $options, $value) { + if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) { + throw new LogicException('Cannot use "intltimezone" input because the PHP intl extension is not available.'); + } + + return $value; + }); + } + + public function getParent(): ?string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'timezone'; + } + + private static function getPhpTimezones(string $input): array + { + $timezones = []; + + foreach (\DateTimeZone::listIdentifiers(\DateTimeZone::ALL) as $timezone) { + if ('intltimezone' === $input && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) { + continue; + } + + $timezones[str_replace(['/', '_'], [' / ', ' '], $timezone)] = $timezone; + } + + return $timezones; + } + + private static function getIntlTimezones(string $input, ?string $locale = null): array + { + $timezones = array_flip(Timezones::getNames($locale)); + + if ('intltimezone' === $input) { + foreach ($timezones as $name => $timezone) { + if ('Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) { + unset($timezones[$name]); + } + } + } + + return $timezones; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/TransformationFailureExtension.php b/lib/symfony/form/Extension/Core/Type/TransformationFailureExtension.php new file mode 100644 index 0000000000..579f419c48 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/TransformationFailureExtension.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\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Christian Flothmann + */ +class TransformationFailureExtension extends AbstractTypeExtension +{ + private ?TranslatorInterface $translator; + + public function __construct(?TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!isset($options['constraints'])) { + $builder->addEventSubscriber(new TransformationFailureListener($this->translator)); + } + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/UlidType.php b/lib/symfony/form/Extension/Core/Type/UlidType.php new file mode 100644 index 0000000000..ea3da07c02 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/UlidType.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\DataTransformer\UlidToStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Pavel Dyakonov + */ +class UlidType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->addViewTransformer(new UlidToStringTransformer()) + ; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'compound' => false, + 'invalid_message' => 'Please enter a valid ULID.', + ]); + } +} diff --git a/lib/symfony/form/Extension/Core/Type/UrlType.php b/lib/symfony/form/Extension/Core/Type/UrlType.php new file mode 100644 index 0000000000..385c7a25fa --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/UrlType.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class UrlType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (null !== $options['default_protocol']) { + $builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol'])); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + if ($options['default_protocol']) { + $view->vars['attr']['inputmode'] = 'url'; + $view->vars['type'] = 'text'; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'default_protocol' => 'http', + 'invalid_message' => 'Please enter a valid URL.', + ]); + + $resolver->setAllowedTypes('default_protocol', ['null', 'string']); + } + + public function getParent(): ?string + { + return TextType::class; + } + + public function getBlockPrefix(): string + { + return 'url'; + } +} diff --git a/lib/symfony/form/Extension/Core/Type/UuidType.php b/lib/symfony/form/Extension/Core/Type/UuidType.php new file mode 100644 index 0000000000..7c0f65b9a0 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/UuidType.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\DataTransformer\UuidToStringTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Pavel Dyakonov + */ +class UuidType extends AbstractType +{ + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->addViewTransformer(new UuidToStringTransformer()) + ; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'compound' => false, + 'invalid_message' => 'Please enter a valid UUID.', + ]); + } +} diff --git a/lib/symfony/form/Extension/Core/Type/WeekType.php b/lib/symfony/form/Extension/Core/Type/WeekType.php new file mode 100644 index 0000000000..66b38cc7c0 --- /dev/null +++ b/lib/symfony/form/Extension/Core/Type/WeekType.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class WeekType extends AbstractType +{ + private const WIDGETS = [ + 'text' => IntegerType::class, + 'choice' => ChoiceType::class, + ]; + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ('string' === $options['input']) { + $builder->addModelTransformer(new WeekToArrayTransformer()); + } + + if ('single_text' === $options['widget']) { + $builder->addViewTransformer(new ReversedTransformer(new WeekToArrayTransformer())); + } else { + $yearOptions = $weekOptions = [ + 'error_bubbling' => true, + ]; + // when the form is compound the entries of the array are ignored in favor of children data + // so we need to handle the cascade setting here + $emptyData = $builder->getEmptyData() ?: []; + + $yearOptions['empty_data'] = $emptyData['year'] ?? ''; + $weekOptions['empty_data'] = $emptyData['week'] ?? ''; + + if (isset($options['invalid_message'])) { + $yearOptions['invalid_message'] = $options['invalid_message']; + $weekOptions['invalid_message'] = $options['invalid_message']; + } + + if (isset($options['invalid_message_parameters'])) { + $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + $weekOptions['invalid_message_parameters'] = $options['invalid_message_parameters']; + } + + if ('choice' === $options['widget']) { + // Only pass a subset of the options to children + $yearOptions['choices'] = array_combine($options['years'], $options['years']); + $yearOptions['placeholder'] = $options['placeholder']['year']; + $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year']; + + $weekOptions['choices'] = array_combine($options['weeks'], $options['weeks']); + $weekOptions['placeholder'] = $options['placeholder']['week']; + $weekOptions['choice_translation_domain'] = $options['choice_translation_domain']['week']; + + // Append generic carry-along options + foreach (['required', 'translation_domain'] as $passOpt) { + $yearOptions[$passOpt] = $options[$passOpt]; + $weekOptions[$passOpt] = $options[$passOpt]; + } + } + + $builder->add('year', self::WIDGETS[$options['widget']], $yearOptions); + $builder->add('week', self::WIDGETS[$options['widget']], $weekOptions); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['widget'] = $options['widget']; + + if ($options['html5']) { + $view->vars['type'] = 'week'; + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; + + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; + + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { + if (\is_array($placeholder)) { + $default = $placeholderDefault($options); + + return array_merge( + ['year' => $default, 'week' => $default], + $placeholder + ); + } + + return [ + 'year' => $placeholder, + 'week' => $placeholder, + ]; + }; + + $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { + if (\is_array($choiceTranslationDomain)) { + $default = false; + + return array_replace( + ['year' => $default, 'week' => $default], + $choiceTranslationDomain + ); + } + + return [ + 'year' => $choiceTranslationDomain, + 'week' => $choiceTranslationDomain, + ]; + }; + + $resolver->setDefaults([ + 'years' => range(date('Y') - 10, date('Y') + 10), + 'weeks' => array_combine(range(1, 53), range(1, 53)), + 'widget' => 'single_text', + 'input' => 'array', + 'placeholder' => $placeholderDefault, + 'html5' => static fn (Options $options) => 'single_text' === $options['widget'], + 'error_bubbling' => false, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', + 'compound' => $compound, + 'choice_translation_domain' => false, + 'invalid_message' => 'Please enter a valid week.', + ]); + + $resolver->setNormalizer('placeholder', $placeholderNormalizer); + $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); + $resolver->setNormalizer('html5', static function (Options $options, $html5) { + if ($html5 && 'single_text' !== $options['widget']) { + throw new LogicException(\sprintf('The "widget" option of "%s" must be set to "single_text" when the "html5" option is enabled.', self::class)); + } + + return $html5; + }); + + $resolver->setAllowedValues('input', [ + 'string', + 'array', + ]); + + $resolver->setAllowedValues('widget', [ + 'single_text', + 'text', + 'choice', + ]); + + $resolver->setAllowedTypes('years', 'int[]'); + $resolver->setAllowedTypes('weeks', 'int[]'); + } + + public function getBlockPrefix(): string + { + return 'week'; + } +} diff --git a/lib/symfony/form/Extension/Csrf/CsrfExtension.php b/lib/symfony/form/Extension/Csrf/CsrfExtension.php new file mode 100644 index 0000000000..0a648f834e --- /dev/null +++ b/lib/symfony/form/Extension/Csrf/CsrfExtension.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Csrf; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * This extension protects forms by using a CSRF token. + * + * @author Bernhard Schussek + */ +class CsrfExtension extends AbstractExtension +{ + private CsrfTokenManagerInterface $tokenManager; + private ?TranslatorInterface $translator; + private ?string $translationDomain; + + public function __construct(CsrfTokenManagerInterface $tokenManager, ?TranslatorInterface $translator = null, ?string $translationDomain = null) + { + $this->tokenManager = $tokenManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain), + ]; + } +} diff --git a/lib/symfony/form/Extension/Csrf/EventListener/CsrfValidationListener.php b/lib/symfony/form/Extension/Csrf/EventListener/CsrfValidationListener.php new file mode 100644 index 0000000000..4cfef76bcc --- /dev/null +++ b/lib/symfony/form/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Csrf\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Util\ServerParams; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Bernhard Schussek + */ +class CsrfValidationListener implements EventSubscriberInterface +{ + private string $fieldName; + private CsrfTokenManagerInterface $tokenManager; + private string $tokenId; + private string $errorMessage; + private ?TranslatorInterface $translator; + private ?string $translationDomain; + private ServerParams $serverParams; + + public static function getSubscribedEvents(): array + { + return [ + FormEvents::PRE_SUBMIT => 'preSubmit', + ]; + } + + public function __construct(string $fieldName, CsrfTokenManagerInterface $tokenManager, string $tokenId, string $errorMessage, ?TranslatorInterface $translator = null, ?string $translationDomain = null, ?ServerParams $serverParams = null) + { + $this->fieldName = $fieldName; + $this->tokenManager = $tokenManager; + $this->tokenId = $tokenId; + $this->errorMessage = $errorMessage; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->serverParams = $serverParams ?? new ServerParams(); + } + + /** + * @return void + */ + public function preSubmit(FormEvent $event) + { + $form = $event->getForm(); + $postRequestSizeExceeded = 'POST' === $form->getConfig()->getMethod() && $this->serverParams->hasPostMaxSizeBeenExceeded(); + + if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) { + $data = $event->getData(); + + $csrfValue = \is_string($data[$this->fieldName] ?? null) ? $data[$this->fieldName] : null; + $csrfToken = new CsrfToken($this->tokenId, $csrfValue); + + if (null === $csrfValue || !$this->tokenManager->isTokenValid($csrfToken)) { + $errorMessage = $this->errorMessage; + + if (null !== $this->translator) { + $errorMessage = $this->translator->trans($errorMessage, [], $this->translationDomain); + } + + $form->addError(new FormError($errorMessage, $errorMessage, [], null, $csrfToken)); + } + + if (\is_array($data)) { + unset($data[$this->fieldName]); + $event->setData($data); + } + } + } +} diff --git a/lib/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/lib/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php new file mode 100644 index 0000000000..09056cc8d5 --- /dev/null +++ b/lib/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Csrf\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Util\ServerParams; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Bernhard Schussek + */ +class FormTypeCsrfExtension extends AbstractTypeExtension +{ + private CsrfTokenManagerInterface $defaultTokenManager; + private bool $defaultEnabled; + private string $defaultFieldName; + private ?TranslatorInterface $translator; + private ?string $translationDomain; + private ?ServerParams $serverParams; + + public function __construct(CsrfTokenManagerInterface $defaultTokenManager, bool $defaultEnabled = true, string $defaultFieldName = '_token', ?TranslatorInterface $translator = null, ?string $translationDomain = null, ?ServerParams $serverParams = null) + { + $this->defaultTokenManager = $defaultTokenManager; + $this->defaultEnabled = $defaultEnabled; + $this->defaultFieldName = $defaultFieldName; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->serverParams = $serverParams; + } + + /** + * Adds a CSRF field to the form when the CSRF protection is enabled. + * + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['csrf_protection']) { + return; + } + + $builder + ->addEventSubscriber(new CsrfValidationListener( + $options['csrf_field_name'], + $options['csrf_token_manager'], + $options['csrf_token_id'] ?: ($builder->getName() ?: $builder->getType()->getInnerType()::class), + $options['csrf_message'], + $this->translator, + $this->translationDomain, + $this->serverParams + )) + ; + } + + /** + * Adds a CSRF field to the root form view. + * + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + if ($options['csrf_protection'] && !$view->parent && $options['compound']) { + $factory = $form->getConfig()->getFormFactory(); + $tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: $form->getConfig()->getType()->getInnerType()::class); + $data = (string) $options['csrf_token_manager']->getToken($tokenId); + + $csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [ + 'block_prefix' => 'csrf_token', + 'mapped' => false, + ]); + + $view->children[$options['csrf_field_name']] = $csrfForm->createView($view); + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'csrf_protection' => $this->defaultEnabled, + 'csrf_field_name' => $this->defaultFieldName, + 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', + 'csrf_token_manager' => $this->defaultTokenManager, + 'csrf_token_id' => null, + ]); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/DataCollector/DataCollectorExtension.php b/lib/symfony/form/Extension/DataCollector/DataCollectorExtension.php new file mode 100644 index 0000000000..50b36bd67f --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/DataCollectorExtension.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\AbstractExtension; + +/** + * Extension for collecting data of the forms on a page. + * + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class DataCollectorExtension extends AbstractExtension +{ + private FormDataCollectorInterface $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\DataCollectorTypeExtension($this->dataCollector), + ]; + } +} diff --git a/lib/symfony/form/Extension/DataCollector/EventListener/DataCollectorListener.php b/lib/symfony/form/Extension/DataCollector/EventListener/DataCollectorListener.php new file mode 100644 index 0000000000..41a52e091e --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/EventListener/DataCollectorListener.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\Form\Extension\DataCollector\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA} + * and {@link FormEvents::POST_SUBMIT} events. + * + * @author Bernhard Schussek + */ +class DataCollectorListener implements EventSubscriberInterface +{ + private FormDataCollectorInterface $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + public static function getSubscribedEvents(): array + { + return [ + // High priority in order to be called as soon as possible + FormEvents::POST_SET_DATA => ['postSetData', 255], + // Low priority in order to be called as late as possible + FormEvents::POST_SUBMIT => ['postSubmit', -255], + ]; + } + + /** + * Listener for the {@link FormEvents::POST_SET_DATA} event. + * + * @return void + */ + public function postSetData(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect basic information about each form + $this->dataCollector->collectConfiguration($event->getForm()); + + // Collect the default data + $this->dataCollector->collectDefaultData($event->getForm()); + } + } + + /** + * Listener for the {@link FormEvents::POST_SUBMIT} event. + * + * @return void + */ + public function postSubmit(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect the submitted data of each form + $this->dataCollector->collectSubmittedData($event->getForm()); + + // Assemble a form tree + // This is done again after the view is built, but we need it here as the view is not always created. + $this->dataCollector->buildPreliminaryFormTree($event->getForm()); + } + } +} diff --git a/lib/symfony/form/Extension/DataCollector/FormDataCollector.php b/lib/symfony/form/Extension/DataCollector/FormDataCollector.php new file mode 100644 index 0000000000..3069304703 --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/FormDataCollector.php @@ -0,0 +1,297 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Caster\ClassStub; +use Symfony\Component\VarDumper\Caster\StubCaster; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Cloner\Stub; + +/** + * Data collector for {@link FormInterface} instances. + * + * @author Robert Schönthal + * @author Bernhard Schussek + * + * @final + */ +class FormDataCollector extends DataCollector implements FormDataCollectorInterface +{ + private FormDataExtractorInterface $dataExtractor; + + /** + * Stores the collected data per {@link FormInterface} instance. + * + * Uses the hashes of the forms as keys. This is preferable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormInterface} instances. + */ + private array $dataByForm; + + /** + * Stores the collected data per {@link FormView} instance. + * + * Uses the hashes of the views as keys. This is preferable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormView} instances. + */ + private array $dataByView; + + /** + * Connects {@link FormView} with {@link FormInterface} instances. + * + * Uses the hashes of the views as keys and the hashes of the forms as + * values. This is preferable over storing the objects directly, because + * this way they can safely be discarded by the GC. + */ + private array $formsByView; + + public function __construct(FormDataExtractorInterface $dataExtractor) + { + if (!class_exists(ClassStub::class)) { + throw new \LogicException(\sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__)); + } + + $this->dataExtractor = $dataExtractor; + + $this->reset(); + } + + /** + * Does nothing. The data is collected during the form event listeners. + */ + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + } + + public function reset(): void + { + $this->data = [ + 'forms' => [], + 'forms_by_hash' => [], + 'nb_errors' => 0, + ]; + } + + public function associateFormWithView(FormInterface $form, FormView $view): void + { + $this->formsByView[spl_object_hash($view)] = spl_object_hash($form); + } + + public function collectConfiguration(FormInterface $form): void + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = []; + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractConfiguration($form) + ); + + foreach ($form as $child) { + $this->collectConfiguration($child); + } + } + + public function collectDefaultData(FormInterface $form): void + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + // field was created by form event + $this->collectConfiguration($form); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractDefaultData($form) + ); + + foreach ($form as $child) { + $this->collectDefaultData($child); + } + } + + public function collectSubmittedData(FormInterface $form): void + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + // field was created by form event + $this->collectConfiguration($form); + $this->collectDefaultData($form); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractSubmittedData($form) + ); + + // Count errors + if (isset($this->dataByForm[$hash]['errors'])) { + $this->data['nb_errors'] += \count($this->dataByForm[$hash]['errors']); + } + + foreach ($form as $child) { + $this->collectSubmittedData($child); + + // Expand current form if there are children with errors + if (empty($this->dataByForm[$hash]['has_children_error'])) { + $childData = $this->dataByForm[spl_object_hash($child)]; + $this->dataByForm[$hash]['has_children_error'] = !empty($childData['has_children_error']) || !empty($childData['errors']); + } + } + } + + public function collectViewVariables(FormView $view): void + { + $hash = spl_object_hash($view); + + if (!isset($this->dataByView[$hash])) { + $this->dataByView[$hash] = []; + } + + $this->dataByView[$hash] = array_replace( + $this->dataByView[$hash], + $this->dataExtractor->extractViewVariables($view) + ); + + foreach ($view->children as $child) { + $this->collectViewVariables($child); + } + } + + public function buildPreliminaryFormTree(FormInterface $form): void + { + $this->data['forms'][$form->getName()] = &$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms_by_hash']); + } + + public function buildFinalFormTree(FormInterface $form, FormView $view): void + { + $this->data['forms'][$form->getName()] = &$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms_by_hash']); + } + + public function getName(): string + { + return 'form'; + } + + public function getData(): array|Data + { + return $this->data; + } + + public function __serialize(): array + { + foreach ($this->data['forms_by_hash'] as &$form) { + if (isset($form['type_class']) && !$form['type_class'] instanceof ClassStub) { + $form['type_class'] = new ClassStub($form['type_class']); + } + } + + return ['data' => $this->data = $this->cloneVar($this->data)]; + } + + protected function getCasters(): array + { + return parent::getCasters() + [ + \Exception::class => static function (\Exception $e, array $a, Stub $s) { + foreach (["\0Exception\0previous", "\0Exception\0trace"] as $k) { + if (isset($a[$k])) { + unset($a[$k]); + ++$s->cut; + } + } + + return $a; + }, + FormInterface::class => static fn (FormInterface $f, array $a) => [ + Caster::PREFIX_VIRTUAL.'name' => $f->getName(), + Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub($f->getConfig()->getType()->getInnerType()::class), + ], + FormView::class => StubCaster::cutInternals(...), + ConstraintViolationInterface::class => static fn (ConstraintViolationInterface $v, array $a) => [ + Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(), + Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(), + Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(), + ], + ]; + } + + private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash): array + { + $hash = spl_object_hash($form); + + $output = &$outputByHash[$hash]; + $output = $this->dataByForm[$hash] + ?? []; + + $output['children'] = []; + + foreach ($form as $name => $child) { + $output['children'][$name] = &$this->recursiveBuildPreliminaryFormTree($child, $outputByHash); + } + + return $output; + } + + private function &recursiveBuildFinalFormTree(?FormInterface $form, FormView $view, array &$outputByHash): array + { + $viewHash = spl_object_hash($view); + $formHash = null; + + if (null !== $form) { + $formHash = spl_object_hash($form); + } elseif (isset($this->formsByView[$viewHash])) { + // The FormInterface instance of the CSRF token is never contained in + // the FormInterface tree of the form, so we need to get the + // corresponding FormInterface instance for its view in a different way + $formHash = $this->formsByView[$viewHash]; + } + if (null !== $formHash) { + $output = &$outputByHash[$formHash]; + } + + $output = $this->dataByView[$viewHash] + ?? []; + + if (null !== $formHash) { + $output = array_replace( + $output, + $this->dataByForm[$formHash] + ?? [] + ); + } + + $output['children'] = []; + + foreach ($view->children as $name => $childView) { + // The CSRF token, for example, is never added to the form tree. + // It is only present in the view. + $childForm = $form?->has($name) ? $form->get($name) : null; + + $output['children'][$name] = &$this->recursiveBuildFinalFormTree($childForm, $childView, $outputByHash); + } + + return $output; + } +} diff --git a/lib/symfony/form/Extension/DataCollector/FormDataCollectorInterface.php b/lib/symfony/form/Extension/DataCollector/FormDataCollectorInterface.php new file mode 100644 index 0000000000..346c101fe3 --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/FormDataCollectorInterface.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * Collects and structures information about forms. + * + * @author Bernhard Schussek + */ +interface FormDataCollectorInterface extends DataCollectorInterface +{ + /** + * Stores configuration data of the given form and its children. + * + * @return void + */ + public function collectConfiguration(FormInterface $form); + + /** + * Stores the default data of the given form and its children. + * + * @return void + */ + public function collectDefaultData(FormInterface $form); + + /** + * Stores the submitted data of the given form and its children. + * + * @return void + */ + public function collectSubmittedData(FormInterface $form); + + /** + * Stores the view variables of the given form view and its children. + * + * @return void + */ + public function collectViewVariables(FormView $view); + + /** + * Specifies that the given objects represent the same conceptual form. + * + * @return void + */ + public function associateFormWithView(FormInterface $form, FormView $view); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * @return void + */ + public function buildPreliminaryFormTree(FormInterface $form); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView} + * object has to be passed. The tree structure of this view object will be + * used for structuring the resulting data. That means, if a child is + * present in the view, but not in the form, it will be present in the final + * data array anyway. + * + * When {@link FormView} instances are present in the view tree, for which + * no corresponding {@link FormInterface} objects can be found in the form + * tree, only the view data will be included in the result. If a + * corresponding {@link FormInterface} exists otherwise, call + * {@link associateFormWithView()} before calling this method. + * + * @return void + */ + public function buildFinalFormTree(FormInterface $form, FormView $view); + + /** + * Returns all collected data. + */ + public function getData(): array|Data; +} diff --git a/lib/symfony/form/Extension/DataCollector/FormDataExtractor.php b/lib/symfony/form/Extension/DataCollector/FormDataExtractor.php new file mode 100644 index 0000000000..158cf32109 --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/FormDataExtractor.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Validator\ConstraintViolationInterface; + +/** + * Default implementation of {@link FormDataExtractorInterface}. + * + * @author Bernhard Schussek + */ +class FormDataExtractor implements FormDataExtractorInterface +{ + public function extractConfiguration(FormInterface $form): array + { + $data = [ + 'id' => $this->buildId($form), + 'name' => $form->getName(), + 'type_class' => $form->getConfig()->getType()->getInnerType()::class, + 'synchronized' => $form->isSynchronized(), + 'passed_options' => [], + 'resolved_options' => [], + ]; + + foreach ($form->getConfig()->getAttribute('data_collector/passed_options', []) as $option => $value) { + $data['passed_options'][$option] = $value; + } + + foreach ($form->getConfig()->getOptions() as $option => $value) { + $data['resolved_options'][$option] = $value; + } + + ksort($data['passed_options']); + ksort($data['resolved_options']); + + return $data; + } + + public function extractDefaultData(FormInterface $form): array + { + $data = [ + 'default_data' => [ + 'norm' => $form->getNormData(), + ], + 'submitted_data' => [], + ]; + + if ($form->getData() !== $form->getNormData()) { + $data['default_data']['model'] = $form->getData(); + } + + if ($form->getViewData() !== $form->getNormData()) { + $data['default_data']['view'] = $form->getViewData(); + } + + return $data; + } + + public function extractSubmittedData(FormInterface $form): array + { + $data = [ + 'submitted_data' => [ + 'norm' => $form->getNormData(), + ], + 'errors' => [], + ]; + + if ($form->getViewData() !== $form->getNormData()) { + $data['submitted_data']['view'] = $form->getViewData(); + } + + if ($form->getData() !== $form->getNormData()) { + $data['submitted_data']['model'] = $form->getData(); + } + + foreach ($form->getErrors() as $error) { + $errorData = [ + 'message' => $error->getMessage(), + 'origin' => \is_object($error->getOrigin()) + ? spl_object_hash($error->getOrigin()) + : null, + 'trace' => [], + ]; + + $cause = $error->getCause(); + + while (null !== $cause) { + if ($cause instanceof ConstraintViolationInterface) { + $errorData['trace'][] = $cause; + $cause = method_exists($cause, 'getCause') ? $cause->getCause() : null; + + continue; + } + + if ($cause instanceof \Exception) { + $errorData['trace'][] = $cause; + $cause = $cause->getPrevious(); + + continue; + } + + $errorData['trace'][] = $cause; + + break; + } + + $data['errors'][] = $errorData; + } + + $data['synchronized'] = $form->isSynchronized(); + + return $data; + } + + public function extractViewVariables(FormView $view): array + { + $data = [ + 'id' => $view->vars['id'] ?? null, + 'name' => $view->vars['name'] ?? null, + 'view_vars' => [], + ]; + + foreach ($view->vars as $varName => $value) { + $data['view_vars'][$varName] = $value; + } + + ksort($data['view_vars']); + + return $data; + } + + /** + * Recursively builds an HTML ID for a form. + */ + private function buildId(FormInterface $form): string + { + $id = $form->getName(); + + if (null !== $form->getParent()) { + $id = $this->buildId($form->getParent()).'_'.$id; + } + + return $id; + } +} diff --git a/lib/symfony/form/Extension/DataCollector/FormDataExtractorInterface.php b/lib/symfony/form/Extension/DataCollector/FormDataExtractorInterface.php new file mode 100644 index 0000000000..d6e46d4670 --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/FormDataExtractorInterface.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\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; + +/** + * Extracts arrays of information out of forms. + * + * @author Bernhard Schussek + */ +interface FormDataExtractorInterface +{ + /** + * Extracts the configuration data of a form. + */ + public function extractConfiguration(FormInterface $form): array; + + /** + * Extracts the default data of a form. + */ + public function extractDefaultData(FormInterface $form): array; + + /** + * Extracts the submitted data of a form. + */ + public function extractSubmittedData(FormInterface $form): array; + + /** + * Extracts the view variables of a form. + */ + public function extractViewVariables(FormView $view): array; +} diff --git a/lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php new file mode 100644 index 0000000000..181a41022e --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Proxy; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Proxy that invokes a data collector when creating a form and its view. + * + * @author Bernhard Schussek + */ +class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface +{ + private ResolvedFormTypeInterface $proxiedType; + private FormDataCollectorInterface $dataCollector; + + public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector) + { + $this->proxiedType = $proxiedType; + $this->dataCollector = $dataCollector; + } + + public function getBlockPrefix(): string + { + return $this->proxiedType->getBlockPrefix(); + } + + public function getParent(): ?ResolvedFormTypeInterface + { + return $this->proxiedType->getParent(); + } + + public function getInnerType(): FormTypeInterface + { + return $this->proxiedType->getInnerType(); + } + + public function getTypeExtensions(): array + { + return $this->proxiedType->getTypeExtensions(); + } + + public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface + { + $builder = $this->proxiedType->createBuilder($factory, $name, $options); + + $builder->setAttribute('data_collector/passed_options', $options); + $builder->setType($this); + + return $builder; + } + + public function createView(FormInterface $form, ?FormView $parent = null): FormView + { + return $this->proxiedType->createView($form, $parent); + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $this->proxiedType->buildForm($builder, $options); + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->buildView($view, $form, $options); + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->finishView($view, $form, $options); + + // Remember which view belongs to which form instance, so that we can + // get the collected data for a view when its form instance is not + // available (e.g. CSRF token) + $this->dataCollector->associateFormWithView($form, $view); + + // Since the CSRF token is only present in the FormView tree, we also + // need to check the FormView tree instead of calling isRoot() on the + // FormInterface tree + if (null === $view->parent) { + $this->dataCollector->collectViewVariables($view); + + // Re-assemble data, in case FormView instances were added, for + // which no FormInterface instances were present (e.g. CSRF token). + // Since finishView() is called after finishing the views of all + // children, we can safely assume that information has been + // collected about the complete form tree. + $this->dataCollector->buildFinalFormTree($form, $view); + } + } + + public function getOptionsResolver(): OptionsResolver + { + return $this->proxiedType->getOptionsResolver(); + } +} diff --git a/lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php new file mode 100644 index 0000000000..f934484124 --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.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\Form\Extension\DataCollector\Proxy; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\Form\ResolvedFormTypeFactoryInterface; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy} + * instances. + * + * @author Bernhard Schussek + */ +class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface +{ + private ResolvedFormTypeFactoryInterface $proxiedFactory; + private FormDataCollectorInterface $dataCollector; + + public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector) + { + $this->proxiedFactory = $proxiedFactory; + $this->dataCollector = $dataCollector; + } + + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface + { + return new ResolvedTypeDataCollectorProxy( + $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent), + $this->dataCollector + ); + } +} diff --git a/lib/symfony/form/Extension/DataCollector/Type/DataCollectorTypeExtension.php b/lib/symfony/form/Extension/DataCollector/Type/DataCollectorTypeExtension.php new file mode 100644 index 0000000000..f1e3c903ec --- /dev/null +++ b/lib/symfony/form/Extension/DataCollector/Type/DataCollectorTypeExtension.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * Type extension for collecting data of a form with this type. + * + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class DataCollectorTypeExtension extends AbstractTypeExtension +{ + private DataCollectorListener $listener; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->listener = new DataCollectorListener($dataCollector); + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->listener); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/DependencyInjection/DependencyInjectionExtension.php b/lib/symfony/form/Extension/DependencyInjection/DependencyInjectionExtension.php new file mode 100644 index 0000000000..ca85ad4348 --- /dev/null +++ b/lib/symfony/form/Extension/DependencyInjection/DependencyInjectionExtension.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DependencyInjection; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\FormExtensionInterface; +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeGuesserChain; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\FormTypeInterface; + +class DependencyInjectionExtension implements FormExtensionInterface +{ + private ?FormTypeGuesserChain $guesser = null; + private bool $guesserLoaded = false; + private ContainerInterface $typeContainer; + private array $typeExtensionServices; + private iterable $guesserServices; + + /** + * @param array> $typeExtensionServices + */ + public function __construct(ContainerInterface $typeContainer, array $typeExtensionServices, iterable $guesserServices) + { + $this->typeContainer = $typeContainer; + $this->typeExtensionServices = $typeExtensionServices; + $this->guesserServices = $guesserServices; + } + + public function getType(string $name): FormTypeInterface + { + if (!$this->typeContainer->has($name)) { + throw new InvalidArgumentException(\sprintf('The field type "%s" is not registered in the service container.', $name)); + } + + return $this->typeContainer->get($name); + } + + public function hasType(string $name): bool + { + return $this->typeContainer->has($name); + } + + public function getTypeExtensions(string $name): array + { + $extensions = []; + + if (isset($this->typeExtensionServices[$name])) { + foreach ($this->typeExtensionServices[$name] as $extension) { + $extensions[] = $extension; + + $extendedTypes = []; + foreach ($extension::getExtendedTypes() as $extendedType) { + $extendedTypes[] = $extendedType; + } + + // validate the result of getExtendedTypes() to ensure it is consistent with the service definition + if (!\in_array($name, $extendedTypes, true)) { + throw new InvalidArgumentException(\sprintf('The extended type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $name, $extension::class, implode('", "', $extendedTypes))); + } + } + } + + return $extensions; + } + + public function hasTypeExtensions(string $name): bool + { + return isset($this->typeExtensionServices[$name]); + } + + public function getTypeGuesser(): ?FormTypeGuesserInterface + { + if (!$this->guesserLoaded) { + $this->guesserLoaded = true; + $guessers = []; + + foreach ($this->guesserServices as $serviceId => $service) { + $guessers[] = $service; + } + + if ($guessers) { + $this->guesser = new FormTypeGuesserChain($guessers); + } + } + + return $this->guesser; + } +} diff --git a/lib/symfony/form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php b/lib/symfony/form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php new file mode 100644 index 0000000000..6c4bf49d6a --- /dev/null +++ b/lib/symfony/form/Extension/HtmlSanitizer/HtmlSanitizerExtension.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HtmlSanitizer; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\AbstractExtension; + +/** + * Integrates the HtmlSanitizer component with the Form library. + * + * @author Nicolas Grekas + */ +class HtmlSanitizerExtension extends AbstractExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\TextTypeHtmlSanitizerExtension($this->sanitizers, $this->defaultSanitizer), + ]; + } +} diff --git a/lib/symfony/form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php b/lib/symfony/form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php new file mode 100644 index 0000000000..8e92ea74a5 --- /dev/null +++ b/lib/symfony/form/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HtmlSanitizer\Type; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Titouan Galopin + */ +class TextTypeHtmlSanitizerExtension extends AbstractTypeExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + public static function getExtendedTypes(): iterable + { + return [TextType::class]; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults(['sanitize_html' => false, 'sanitizer' => null]) + ->setAllowedTypes('sanitize_html', 'bool') + ->setAllowedTypes('sanitizer', ['string', 'null']) + ; + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['sanitize_html']) { + return; + } + + $sanitizers = $this->sanitizers; + $sanitizer = $options['sanitizer'] ?? $this->defaultSanitizer; + + $builder->addEventListener( + FormEvents::PRE_SUBMIT, + static function (FormEvent $event) use ($sanitizers, $sanitizer) { + if (\is_scalar($data = $event->getData()) && '' !== trim($data)) { + $event->setData($sanitizers->get($sanitizer)->sanitize($data)); + } + }, + 10000 /* as soon as possible */ + ); + } +} diff --git a/lib/symfony/form/Extension/HttpFoundation/HttpFoundationExtension.php b/lib/symfony/form/Extension/HttpFoundation/HttpFoundationExtension.php new file mode 100644 index 0000000000..85bc4f4720 --- /dev/null +++ b/lib/symfony/form/Extension/HttpFoundation/HttpFoundationExtension.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HttpFoundation; + +use Symfony\Component\Form\AbstractExtension; + +/** + * Integrates the HttpFoundation component with the Form library. + * + * @author Bernhard Schussek + */ +class HttpFoundationExtension extends AbstractExtension +{ + protected function loadTypeExtensions(): array + { + return [ + new Type\FormTypeHttpFoundationExtension(), + ]; + } +} diff --git a/lib/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php b/lib/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php new file mode 100644 index 0000000000..fd2ecb0189 --- /dev/null +++ b/lib/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HttpFoundation; + +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\RequestHandlerInterface; +use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\Form\Util\ServerParams; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; + +/** + * A request processor using the {@link Request} class of the HttpFoundation + * component. + * + * @author Bernhard Schussek + */ +class HttpFoundationRequestHandler implements RequestHandlerInterface +{ + private ServerParams $serverParams; + + public function __construct(?ServerParams $serverParams = null) + { + $this->serverParams = $serverParams ?? new ServerParams(); + } + + /** + * @return void + */ + public function handleRequest(FormInterface $form, mixed $request = null) + { + if (!$request instanceof Request) { + throw new UnexpectedTypeException($request, Request::class); + } + + $name = $form->getName(); + $method = $form->getConfig()->getMethod(); + + if ($method !== $request->getMethod()) { + return; + } + + // For request methods that must not have a request body we fetch data + // from the query string. Otherwise we look for data in the request body. + if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) { + if ('' === $name) { + $data = $request->query->all(); + } else { + // Don't submit GET requests if the form's name does not exist + // in the request + if (!$request->query->has($name)) { + return; + } + + $data = $request->query->all()[$name]; + } + } else { + // Mark the form with an error if the uploaded size was too large + // This is done here and not in FormValidator because $_POST is + // empty when that error occurs. Hence the form is never submitted. + if ($this->serverParams->hasPostMaxSizeBeenExceeded()) { + // Submit the form, but don't clear the default values + $form->submit(null, false); + + $form->addError(new FormError( + $form->getConfig()->getOption('upload_max_size_message')(), + null, + ['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()] + )); + + return; + } + + if ('' === $name) { + $params = $request->request->all(); + $files = $request->files->all(); + } elseif ($request->request->has($name) || $request->files->has($name)) { + $default = $form->getConfig()->getCompound() ? [] : null; + $params = $request->request->all()[$name] ?? $default; + $files = $request->files->get($name, $default); + } else { + // Don't submit the form if it is not present in the request + return; + } + + if (\is_array($params) && \is_array($files)) { + $data = FormUtil::mergeParamsAndFiles($params, $files); + } else { + $data = $params ?: $files; + } + } + + // Don't auto-submit the form unless at least one field is present. + if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) { + return; + } + + $form->submit($data, 'PATCH' !== $method); + } + + public function isFileUpload(mixed $data): bool + { + return $data instanceof File; + } + + public function getUploadFileError(mixed $data): ?int + { + if (!$data instanceof UploadedFile || $data->isValid()) { + return null; + } + + return $data->getError(); + } +} diff --git a/lib/symfony/form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php b/lib/symfony/form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php new file mode 100644 index 0000000000..8222655192 --- /dev/null +++ b/lib/symfony/form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HttpFoundation\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\RequestHandlerInterface; + +/** + * @author Bernhard Schussek + */ +class FormTypeHttpFoundationExtension extends AbstractTypeExtension +{ + private RequestHandlerInterface $requestHandler; + + public function __construct(?RequestHandlerInterface $requestHandler = null) + { + $this->requestHandler = $requestHandler ?? new HttpFoundationRequestHandler(); + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->setRequestHandler($this->requestHandler); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/PasswordHasher/EventListener/PasswordHasherListener.php b/lib/symfony/form/Extension/PasswordHasher/EventListener/PasswordHasherListener.php new file mode 100644 index 0000000000..f336101ba2 --- /dev/null +++ b/lib/symfony/form/Extension/PasswordHasher/EventListener/PasswordHasherListener.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener; + +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +/** + * @author Sébastien Alfaiate + * @author Gábor Egyed + */ +class PasswordHasherListener +{ + private array $passwords = []; + + public function __construct( + private UserPasswordHasherInterface $passwordHasher, + private ?PropertyAccessorInterface $propertyAccessor = null, + ) { + $this->propertyAccessor ??= PropertyAccess::createPropertyAccessor(); + } + + /** + * @return void + */ + public function registerPassword(FormEvent $event) + { + if (null === $event->getData() || '' === $event->getData()) { + return; + } + + $this->assertNotMapped($event->getForm()); + + $this->passwords[] = [ + 'form' => $event->getForm(), + 'property_path' => $event->getForm()->getConfig()->getOption('hash_property_path'), + 'password' => $event->getData(), + ]; + } + + /** + * @return void + */ + public function hashPasswords(FormEvent $event) + { + $form = $event->getForm(); + + if (!$form->isRoot()) { + return; + } + + if ($form->isValid()) { + foreach ($this->passwords as $password) { + $user = $this->getUser($password['form']); + + $this->propertyAccessor->setValue( + $user, + $password['property_path'], + $this->passwordHasher->hashPassword($user, $password['password']) + ); + } + } + + $this->passwords = []; + } + + private function getTargetForm(FormInterface $form): FormInterface + { + if (!$parentForm = $form->getParent()) { + return $form; + } + + $parentType = $parentForm->getConfig()->getType(); + + do { + if ($parentType->getInnerType() instanceof RepeatedType) { + return $parentForm; + } + } while ($parentType = $parentType->getParent()); + + return $form; + } + + private function getUser(FormInterface $form): PasswordAuthenticatedUserInterface + { + $parent = $this->getTargetForm($form)->getParent(); + + if (!($user = $parent?->getData()) || !$user instanceof PasswordAuthenticatedUserInterface) { + throw new InvalidConfigurationException(\sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user))); + } + + return $user; + } + + private function assertNotMapped(FormInterface $form): void + { + if ($this->getTargetForm($form)->getConfig()->getMapped()) { + throw new InvalidConfigurationException('The "hash_property_path" option cannot be used on mapped field.'); + } + } +} diff --git a/lib/symfony/form/Extension/PasswordHasher/PasswordHasherExtension.php b/lib/symfony/form/Extension/PasswordHasher/PasswordHasherExtension.php new file mode 100644 index 0000000000..b9675c2153 --- /dev/null +++ b/lib/symfony/form/Extension/PasswordHasher/PasswordHasherExtension.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; + +/** + * Integrates the PasswordHasher component with the Form library. + * + * @author Sébastien Alfaiate + */ +class PasswordHasherExtension extends AbstractExtension +{ + public function __construct( + private PasswordHasherListener $passwordHasherListener, + ) { + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\FormTypePasswordHasherExtension($this->passwordHasherListener), + new Type\PasswordTypePasswordHasherExtension($this->passwordHasherListener), + ]; + } +} diff --git a/lib/symfony/form/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php b/lib/symfony/form/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php new file mode 100644 index 0000000000..5308992863 --- /dev/null +++ b/lib/symfony/form/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; + +/** + * @author Sébastien Alfaiate + */ +class FormTypePasswordHasherExtension extends AbstractTypeExtension +{ + public function __construct( + private PasswordHasherListener $passwordHasherListener, + ) { + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'hashPasswords']); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php b/lib/symfony/form/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php new file mode 100644 index 0000000000..6f022fb1bf --- /dev/null +++ b/lib/symfony/form/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * @author Sébastien Alfaiate + */ +class PasswordTypePasswordHasherExtension extends AbstractTypeExtension +{ + public function __construct( + private PasswordHasherListener $passwordHasherListener, + ) { + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($options['hash_property_path']) { + $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'registerPassword']); + } + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'hash_property_path' => null, + ]); + + $resolver->setAllowedTypes('hash_property_path', ['null', 'string', PropertyPath::class]); + + $resolver->setInfo('hash_property_path', 'A valid PropertyAccess syntax where the hashed password will be set.'); + } + + public static function getExtendedTypes(): iterable + { + return [PasswordType::class]; + } +} diff --git a/lib/symfony/form/Extension/Validator/Constraints/Form.php b/lib/symfony/form/Extension/Validator/Constraints/Form.php new file mode 100644 index 0000000000..6dec01be22 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Constraints/Form.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * @author Bernhard Schussek + */ +class Form extends Constraint +{ + public const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca'; + public const NO_SUCH_FIELD_ERROR = '6e5212ed-a197-4339-99aa-5654798a4854'; + + protected const ERROR_NAMES = [ + self::NOT_SYNCHRONIZED_ERROR => 'NOT_SYNCHRONIZED_ERROR', + self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR', + ]; + + /** + * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + */ + protected static $errorNames = self::ERROR_NAMES; + + public function getTargets(): string|array + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/lib/symfony/form/Extension/Validator/Constraints/FormValidator.php b/lib/symfony/form/Extension/Validator/Constraints/FormValidator.php new file mode 100644 index 0000000000..944fc2d6be --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Constraints/FormValidator.php @@ -0,0 +1,279 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Constraints; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Composite; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +/** + * @author Bernhard Schussek + */ +class FormValidator extends ConstraintValidator +{ + /** + * @var \SplObjectStorage> + */ + private \SplObjectStorage $resolvedGroups; + + /** + * @return void + */ + public function validate(mixed $form, Constraint $formConstraint) + { + if (!$formConstraint instanceof Form) { + throw new UnexpectedTypeException($formConstraint, Form::class); + } + + if (!$form instanceof FormInterface) { + return; + } + + /** @var FormInterface $form */ + $config = $form->getConfig(); + + $validator = $this->context->getValidator()->inContext($this->context); + + if ($form->isSubmitted() && $form->isSynchronized()) { + // Validate the form data only if transformation succeeded + $groups = $this->getValidationGroups($form); + + if (!$groups) { + return; + } + + $data = $form->getData(); + // Validate the data against its own constraints + $validateDataGraph = $form->isRoot() + && (\is_object($data) || \is_array($data)) + && (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups)) + ; + + // Validate the data against the constraints defined in the form + /** @var Constraint[] $constraints */ + $constraints = $config->getOption('constraints', []); + + $hasChildren = $form->count() > 0; + + if ($hasChildren && $form->isRoot()) { + $this->resolvedGroups = new \SplObjectStorage(); + } + + if ($groups instanceof GroupSequence) { + // Validate the data, the form AND nested fields in sequence + $violationsCount = $this->context->getViolations()->count(); + + foreach ($groups->groups as $group) { + if ($validateDataGraph) { + $validator->atPath('data')->validate($data, null, $group); + } + + if ($groupedConstraints = self::getConstraintsInGroups($constraints, $group)) { + $validator->atPath('data')->validate($data, $groupedConstraints, $group); + } + + foreach ($form->all() as $field) { + if ($field->isSubmitted()) { + // remember to validate this field in one group only + // otherwise resolving the groups would reuse the same + // sequence recursively, thus some fields could fail + // in different steps without breaking early enough + $this->resolvedGroups[$field] = (array) $group; + $fieldFormConstraint = new Form(); + $fieldFormConstraint->groups = $group; + $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath()); + $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group); + } + } + + if ($violationsCount < $this->context->getViolations()->count()) { + break; + } + } + } else { + if ($validateDataGraph) { + $validator->atPath('data')->validate($data, null, $groups); + } + + $groupedConstraints = []; + + foreach ($constraints as $constraint) { + // For the "Valid" constraint, validate the data in all groups + if ($constraint instanceof Valid) { + if (\is_object($data) || \is_array($data)) { + $validator->atPath('data')->validate($data, $constraint, $groups); + } + + continue; + } + + // Otherwise validate a constraint only once for the first + // matching group + foreach ($groups as $group) { + if (\in_array($group, $constraint->groups)) { + $groupedConstraints[$group][] = $constraint; + + // Prevent duplicate validation + if (!$constraint instanceof Composite) { + continue 2; + } + } + } + } + + foreach ($groupedConstraints as $group => $constraint) { + $validator->atPath('data')->validate($data, $constraint, $group); + } + + foreach ($form->all() as $field) { + if ($field->isSubmitted()) { + $this->resolvedGroups[$field] = $groups; + $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath()); + $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint); + } + } + } + + if ($hasChildren && $form->isRoot()) { + // destroy storage to avoid memory leaks + $this->resolvedGroups = new \SplObjectStorage(); + } + } elseif (!$form->isSynchronized()) { + $childrenSynchronized = true; + + /** @var FormInterface $child */ + foreach ($form as $child) { + if (!$child->isSynchronized()) { + $childrenSynchronized = false; + $this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath()); + $validator->atPath(\sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint); + } + } + + // Mark the form with an error if it is not synchronized BUT all + // of its children are synchronized. If any child is not + // synchronized, an error is displayed there already and showing + // a second error in its parent form is pointless, or worse, may + // lead to duplicate errors if error bubbling is enabled on the + // child. + // See also https://github.com/symfony/symfony/issues/4359 + if ($childrenSynchronized) { + $clientDataAsString = \is_scalar($form->getViewData()) + ? (string) $form->getViewData() + : get_debug_type($form->getViewData()); + + $failure = $form->getTransformationFailure(); + + $this->context->setConstraint($formConstraint); + $this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message')) + ->setParameters(array_replace( + ['{{ value }}' => $clientDataAsString], + $config->getOption('invalid_message_parameters'), + $failure->getInvalidMessageParameters() + )) + ->setInvalidValue($form->getViewData()) + ->setCode(Form::NOT_SYNCHRONIZED_ERROR) + ->setCause($failure) + ->addViolation(); + } + } + + // Mark the form with an error if it contains extra fields + if (!$config->getOption('allow_extra_fields') && \count($form->getExtraData()) > 0) { + $this->context->setConstraint($formConstraint); + $this->context->buildViolation($config->getOption('extra_fields_message', '')) + ->setParameter('{{ extra_fields }}', '"'.implode('", "', array_keys($form->getExtraData())).'"') + ->setPlural(\count($form->getExtraData())) + ->setInvalidValue($form->getExtraData()) + ->setCode(Form::NO_SUCH_FIELD_ERROR) + ->addViolation(); + } + } + + /** + * Returns the validation groups of the given form. + * + * @return string|GroupSequence|array + */ + private function getValidationGroups(FormInterface $form): string|GroupSequence|array + { + // Determine the clicked button of the complete form tree + $clickedButton = null; + + if (method_exists($form, 'getClickedButton')) { + $clickedButton = $form->getClickedButton(); + } + + if (null !== $clickedButton) { + $groups = $clickedButton->getConfig()->getOption('validation_groups'); + + if (null !== $groups) { + return self::resolveValidationGroups($groups, $form); + } + } + + do { + $groups = $form->getConfig()->getOption('validation_groups'); + + if (null !== $groups) { + return self::resolveValidationGroups($groups, $form); + } + + if (isset($this->resolvedGroups[$form])) { + return $this->resolvedGroups[$form]; + } + + $form = $form->getParent(); + } while (null !== $form); + + return [Constraint::DEFAULT_GROUP]; + } + + /** + * Post-processes the validation groups option for a given form. + * + * @param string|GroupSequence|array|callable $groups The validation groups + * + * @return GroupSequence|array + */ + private static function resolveValidationGroups(string|GroupSequence|array|callable $groups, FormInterface $form): GroupSequence|array + { + if (!\is_string($groups) && \is_callable($groups)) { + $groups = $groups($form); + } + + if ($groups instanceof GroupSequence) { + return $groups; + } + + return (array) $groups; + } + + private static function getConstraintsInGroups(array $constraints, string|array $group): array + { + $groups = (array) $group; + + return array_filter($constraints, static function (Constraint $constraint) use ($groups) { + foreach ($groups as $group) { + if (\in_array($group, $constraint->groups, true)) { + return true; + } + } + + return false; + }); + } +} diff --git a/lib/symfony/form/Extension/Validator/EventListener/ValidationListener.php b/lib/symfony/form/Extension/Validator/EventListener/ValidationListener.php new file mode 100644 index 0000000000..e2d4357622 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/EventListener/ValidationListener.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\Form\Extension\Validator\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @author Bernhard Schussek + */ +class ValidationListener implements EventSubscriberInterface +{ + private ValidatorInterface $validator; + private ViolationMapperInterface $violationMapper; + + public static function getSubscribedEvents(): array + { + return [FormEvents::POST_SUBMIT => 'validateForm']; + } + + public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper) + { + $this->validator = $validator; + $this->violationMapper = $violationMapper; + } + + /** + * @return void + */ + public function validateForm(FormEvent $event) + { + $form = $event->getForm(); + + if ($form->isRoot()) { + // Form groups are validated internally (FormValidator). Here we don't set groups as they are retrieved into the validator. + foreach ($this->validator->validate($form) as $violation) { + // Allow the "invalid" constraint to be put onto + // non-synchronized forms + $allowNonSynchronized = $violation->getConstraint() instanceof Form && Form::NOT_SYNCHRONIZED_ERROR === $violation->getCode(); + + $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized); + } + } + } +} diff --git a/lib/symfony/form/Extension/Validator/Type/BaseValidatorExtension.php b/lib/symfony/form/Extension/Validator/Type/BaseValidatorExtension.php new file mode 100644 index 0000000000..ea01d03699 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Type/BaseValidatorExtension.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\GroupSequence; + +/** + * Encapsulates common logic of {@link FormTypeValidatorExtension} and + * {@link SubmitTypeValidatorExtension}. + * + * @author Bernhard Schussek + */ +abstract class BaseValidatorExtension extends AbstractTypeExtension +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + // Make sure that validation groups end up as null, closure or array + $validationGroupsNormalizer = static function (Options $options, $groups) { + if (false === $groups) { + return []; + } + + if (empty($groups)) { + return null; + } + + if (\is_callable($groups)) { + return $groups; + } + + if ($groups instanceof GroupSequence) { + return $groups; + } + + return (array) $groups; + }; + + $resolver->setDefaults([ + 'validation_groups' => null, + ]); + + $resolver->setNormalizer('validation_groups', $validationGroupsNormalizer); + } +} diff --git a/lib/symfony/form/Extension/Validator/Type/FormTypeValidatorExtension.php b/lib/symfony/form/Extension/Validator/Type/FormTypeValidatorExtension.php new file mode 100644 index 0000000000..a1fd686d53 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Type; + +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; +use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormRendererInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Bernhard Schussek + */ +class FormTypeValidatorExtension extends BaseValidatorExtension +{ + private ValidatorInterface $validator; + private ViolationMapper $violationMapper; + private bool $legacyErrorMessages; + + public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, ?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) + { + $this->validator = $validator; + $this->violationMapper = new ViolationMapper($formRenderer, $translator); + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper)); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + // Constraint should always be converted to an array + $constraintsNormalizer = static fn (Options $options, $constraints) => \is_object($constraints) ? [$constraints] : (array) $constraints; + + $resolver->setDefaults([ + 'error_mapping' => [], + 'constraints' => [], + 'invalid_message' => 'This value is not valid.', + 'invalid_message_parameters' => [], + 'allow_extra_fields' => false, + 'extra_fields_message' => 'This form should not contain extra fields.', + ]); + $resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']); + $resolver->setNormalizer('constraints', $constraintsNormalizer); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php b/lib/symfony/form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php new file mode 100644 index 0000000000..d41dc0168c --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Type/RepeatedTypeValidatorExtension.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\Form\Extension\Validator\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Bernhard Schussek + */ +class RepeatedTypeValidatorExtension extends AbstractTypeExtension +{ + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + // Map errors to the first field + $errorMapping = static fn (Options $options) => ['.' => $options['first_name']]; + + $resolver->setDefaults([ + 'error_mapping' => $errorMapping, + ]); + } + + public static function getExtendedTypes(): iterable + { + return [RepeatedType::class]; + } +} diff --git a/lib/symfony/form/Extension/Validator/Type/SubmitTypeValidatorExtension.php b/lib/symfony/form/Extension/Validator/Type/SubmitTypeValidatorExtension.php new file mode 100644 index 0000000000..8efae7d52e --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Type/SubmitTypeValidatorExtension.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\Form\Extension\Validator\Type; + +use Symfony\Component\Form\Extension\Core\Type\SubmitType; + +/** + * @author Bernhard Schussek + */ +class SubmitTypeValidatorExtension extends BaseValidatorExtension +{ + public static function getExtendedTypes(): iterable + { + return [SubmitType::class]; + } +} diff --git a/lib/symfony/form/Extension/Validator/Type/UploadValidatorExtension.php b/lib/symfony/form/Extension/Validator/Type/UploadValidatorExtension.php new file mode 100644 index 0000000000..184bebbafa --- /dev/null +++ b/lib/symfony/form/Extension/Validator/Type/UploadValidatorExtension.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\Form\Extension\Validator\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Abdellatif Ait boudad + * @author David Badura + */ +class UploadValidatorExtension extends AbstractTypeExtension +{ + private TranslatorInterface $translator; + private ?string $translationDomain; + + public function __construct(TranslatorInterface $translator, ?string $translationDomain = null) + { + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $translator = $this->translator; + $translationDomain = $this->translationDomain; + $resolver->setNormalizer('upload_max_size_message', static fn (Options $options, $message) => static fn () => $translator->trans($message(), [], $translationDomain)); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/lib/symfony/form/Extension/Validator/ValidatorExtension.php b/lib/symfony/form/Extension/Validator/ValidatorExtension.php new file mode 100644 index 0000000000..bfad8074fc --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ValidatorExtension.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\FormRendererInterface; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Extension supporting the Symfony Validator component in forms. + * + * @author Bernhard Schussek + */ +class ValidatorExtension extends AbstractExtension +{ + private ValidatorInterface $validator; + private ?FormRendererInterface $formRenderer; + private ?TranslatorInterface $translator; + private bool $legacyErrorMessages; + + public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, ?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) + { + $this->legacyErrorMessages = $legacyErrorMessages; + + /** @var ClassMetadata $metadata */ + $metadata = $validator->getMetadataFor(\Symfony\Component\Form\Form::class); + + // Register the form constraints in the validator programmatically. + // This functionality is required when using the Form component without + // the DIC, where the XML file is loaded automatically. Thus the following + // code must be kept synchronized with validation.xml + + $metadata->addConstraint(new Form()); + $metadata->addConstraint(new Traverse(false)); + + $this->validator = $validator; + $this->formRenderer = $formRenderer; + $this->translator = $translator; + } + + public function loadTypeGuesser(): ?FormTypeGuesserInterface + { + return new ValidatorTypeGuesser($this->validator); + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages, $this->formRenderer, $this->translator), + new Type\RepeatedTypeValidatorExtension(), + new Type\SubmitTypeValidatorExtension(), + ]; + } +} diff --git a/lib/symfony/form/Extension/Validator/ValidatorTypeGuesser.php b/lib/symfony/form/Extension/Validator/ValidatorTypeGuesser.php new file mode 100644 index 0000000000..ba3530d3d1 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ValidatorTypeGuesser.php @@ -0,0 +1,293 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator; + +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\CurrencyType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Form\Extension\Core\Type\LocaleType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TimeType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Guess\ValueGuess; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Count; +use Symfony\Component\Validator\Constraints\Country; +use Symfony\Component\Validator\Constraints\Currency; +use Symfony\Component\Validator\Constraints\Date; +use Symfony\Component\Validator\Constraints\DateTime; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\Constraints\Image; +use Symfony\Component\Validator\Constraints\Ip; +use Symfony\Component\Validator\Constraints\IsFalse; +use Symfony\Component\Validator\Constraints\IsTrue; +use Symfony\Component\Validator\Constraints\Language; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\Locale; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Constraints\Time; +use Symfony\Component\Validator\Constraints\Type; +use Symfony\Component\Validator\Constraints\Url; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; + +class ValidatorTypeGuesser implements FormTypeGuesserInterface +{ + private MetadataFactoryInterface $metadataFactory; + + public function __construct(MetadataFactoryInterface $metadataFactory) + { + $this->metadataFactory = $metadataFactory; + } + + public function guessType(string $class, string $property): ?TypeGuess + { + return $this->guess($class, $property, $this->guessTypeForConstraint(...)); + } + + public function guessRequired(string $class, string $property): ?ValueGuess + { + // If we don't find any constraint telling otherwise, we can assume + // that a field is not required (with LOW_CONFIDENCE) + return $this->guess($class, $property, $this->guessRequiredForConstraint(...), false); + } + + public function guessMaxLength(string $class, string $property): ?ValueGuess + { + return $this->guess($class, $property, $this->guessMaxLengthForConstraint(...)); + } + + public function guessPattern(string $class, string $property): ?ValueGuess + { + return $this->guess($class, $property, $this->guessPatternForConstraint(...)); + } + + /** + * Guesses a field class name for a given constraint. + */ + public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess + { + switch ($constraint::class) { + case Type::class: + switch ($constraint->type) { + case 'array': + return new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE); + case 'boolean': + case 'bool': + return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE); + + case 'double': + case 'float': + case 'numeric': + case 'real': + return new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE); + + case 'integer': + case 'int': + case 'long': + return new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE); + + case \DateTime::class: + case '\DateTime': + return new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE); + + case \DateTimeImmutable::class: + case '\DateTimeImmutable': + case \DateTimeInterface::class: + case '\DateTimeInterface': + return new TypeGuess(DateType::class, ['input' => 'datetime_immutable'], Guess::MEDIUM_CONFIDENCE); + + case 'string': + return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE); + } + break; + + case Country::class: + return new TypeGuess(CountryType::class, [], Guess::HIGH_CONFIDENCE); + + case Currency::class: + return new TypeGuess(CurrencyType::class, [], Guess::HIGH_CONFIDENCE); + + case Date::class: + return new TypeGuess(DateType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE); + + case DateTime::class: + return new TypeGuess(DateTimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE); + + case Email::class: + return new TypeGuess(EmailType::class, [], Guess::HIGH_CONFIDENCE); + + case File::class: + case Image::class: + $options = []; + if ($constraint->mimeTypes) { + $options = ['attr' => ['accept' => implode(',', (array) $constraint->mimeTypes)]]; + } + + return new TypeGuess(FileType::class, $options, Guess::HIGH_CONFIDENCE); + + case Language::class: + return new TypeGuess(LanguageType::class, [], Guess::HIGH_CONFIDENCE); + + case Locale::class: + return new TypeGuess(LocaleType::class, [], Guess::HIGH_CONFIDENCE); + + case Time::class: + return new TypeGuess(TimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE); + + case Url::class: + return new TypeGuess(UrlType::class, [], Guess::HIGH_CONFIDENCE); + + case Ip::class: + return new TypeGuess(TextType::class, [], Guess::MEDIUM_CONFIDENCE); + + case Length::class: + case Regex::class: + return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE); + + case Range::class: + return new TypeGuess(NumberType::class, [], Guess::LOW_CONFIDENCE); + + case Count::class: + return new TypeGuess(CollectionType::class, [], Guess::LOW_CONFIDENCE); + + case IsTrue::class: + case IsFalse::class: + return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE); + } + + return null; + } + + /** + * Guesses whether a field is required based on the given constraint. + */ + public function guessRequiredForConstraint(Constraint $constraint): ?ValueGuess + { + return match ($constraint::class) { + NotNull::class, + NotBlank::class, + IsTrue::class => new ValueGuess(true, Guess::HIGH_CONFIDENCE), + default => null, + }; + } + + /** + * Guesses a field's maximum length based on the given constraint. + */ + public function guessMaxLengthForConstraint(Constraint $constraint): ?ValueGuess + { + switch ($constraint::class) { + case Length::class: + if (is_numeric($constraint->max)) { + return new ValueGuess($constraint->max, Guess::HIGH_CONFIDENCE); + } + break; + + case Type::class: + if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) { + return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); + } + break; + + case Range::class: + if (is_numeric($constraint->max)) { + return new ValueGuess(\strlen((string) $constraint->max), Guess::LOW_CONFIDENCE); + } + break; + } + + return null; + } + + /** + * Guesses a field's pattern based on the given constraint. + */ + public function guessPatternForConstraint(Constraint $constraint): ?ValueGuess + { + switch ($constraint::class) { + case Length::class: + if (is_numeric($constraint->min)) { + return new ValueGuess(\sprintf('.{%s,}', (string) $constraint->min), Guess::LOW_CONFIDENCE); + } + break; + + case Regex::class: + $htmlPattern = $constraint->getHtmlPattern(); + + if (null !== $htmlPattern) { + return new ValueGuess($htmlPattern, Guess::HIGH_CONFIDENCE); + } + break; + + case Range::class: + if (is_numeric($constraint->min)) { + return new ValueGuess(\sprintf('.{%s,}', \strlen((string) $constraint->min)), Guess::LOW_CONFIDENCE); + } + break; + + case Type::class: + if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) { + return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); + } + break; + } + + return null; + } + + /** + * Iterates over the constraints of a property, executes a constraints on + * them and returns the best guess. + * + * @param \Closure $closure The closure that returns a guess + * for a given constraint + * @param mixed $defaultValue The default value assumed if no other value + * can be guessed + */ + protected function guess(string $class, string $property, \Closure $closure, mixed $defaultValue = null): ?Guess + { + $guesses = []; + $classMetadata = $this->metadataFactory->getMetadataFor($class); + + if ($classMetadata instanceof ClassMetadataInterface && $classMetadata->hasPropertyMetadata($property)) { + foreach ($classMetadata->getPropertyMetadata($property) as $memberMetadata) { + foreach ($memberMetadata->getConstraints() as $constraint) { + if ($guess = $closure($constraint)) { + $guesses[] = $guess; + } + } + } + } + + if (null !== $defaultValue) { + $guesses[] = new ValueGuess($defaultValue, Guess::LOW_CONFIDENCE); + } + + return Guess::getBestGuess($guesses); + } +} diff --git a/lib/symfony/form/Extension/Validator/ViolationMapper/MappingRule.php b/lib/symfony/form/Extension/Validator/ViolationMapper/MappingRule.php new file mode 100644 index 0000000000..a45241e938 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ViolationMapper/MappingRule.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\Form\Exception\ErrorMappingException; +use Symfony\Component\Form\FormInterface; + +/** + * @author Bernhard Schussek + */ +class MappingRule +{ + private FormInterface $origin; + private string $propertyPath; + private string $targetPath; + + public function __construct(FormInterface $origin, string $propertyPath, string $targetPath) + { + $this->origin = $origin; + $this->propertyPath = $propertyPath; + $this->targetPath = $targetPath; + } + + public function getOrigin(): FormInterface + { + return $this->origin; + } + + /** + * Matches a property path against the rule path. + * + * If the rule matches, the form mapped by the rule is returned. + * Otherwise this method returns false. + */ + public function match(string $propertyPath): ?FormInterface + { + return $propertyPath === $this->propertyPath ? $this->getTarget() : null; + } + + /** + * Matches a property path against a prefix of the rule path. + */ + public function isPrefix(string $propertyPath): bool + { + $length = \strlen($propertyPath); + $prefix = substr($this->propertyPath, 0, $length); + $next = $this->propertyPath[$length] ?? null; + + return $prefix === $propertyPath && ('[' === $next || '.' === $next); + } + + /** + * @throws ErrorMappingException + */ + public function getTarget(): FormInterface + { + $childNames = explode('.', $this->targetPath); + $target = $this->origin; + + foreach ($childNames as $childName) { + if (!$target->has($childName)) { + throw new ErrorMappingException(\sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName())); + } + $target = $target->get($childName); + } + + return $target; + } +} diff --git a/lib/symfony/form/Extension/Validator/ViolationMapper/RelativePath.php b/lib/symfony/form/Extension/Validator/ViolationMapper/RelativePath.php new file mode 100644 index 0000000000..0384edb444 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ViolationMapper/RelativePath.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * @author Bernhard Schussek + */ +class RelativePath extends PropertyPath +{ + private FormInterface $root; + + public function __construct(FormInterface $root, string $propertyPath) + { + parent::__construct($propertyPath); + + $this->root = $root; + } + + public function getRoot(): FormInterface + { + return $this->root; + } +} diff --git a/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapper.php b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapper.php new file mode 100644 index 0000000000..ccad21fef3 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -0,0 +1,343 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\Form\FileUploadError; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormRendererInterface; +use Symfony\Component\Form\Util\InheritDataAwareIterator; +use Symfony\Component\PropertyAccess\PropertyPathBuilder; +use Symfony\Component\PropertyAccess\PropertyPathIterator; +use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface; +use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Bernhard Schussek + */ +class ViolationMapper implements ViolationMapperInterface +{ + private ?FormRendererInterface $formRenderer; + private ?TranslatorInterface $translator; + private bool $allowNonSynchronized = false; + + public function __construct(?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) + { + $this->formRenderer = $formRenderer; + $this->translator = $translator; + } + + /** + * @return void + */ + public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false) + { + $this->allowNonSynchronized = $allowNonSynchronized; + + // The scope is the currently found most specific form that + // an error should be mapped to. After setting the scope, the + // mapper will try to continue to find more specific matches in + // the children of scope. If it cannot, the error will be + // mapped to this scope. + $scope = null; + + $violationPath = null; + $relativePath = null; + $match = false; + + // Don't create a ViolationPath instance for empty property paths + if ('' !== $violation->getPropertyPath()) { + $violationPath = new ViolationPath($violation->getPropertyPath()); + $relativePath = $this->reconstructPath($violationPath, $form); + } + + // This case happens if the violation path is empty and thus + // the violation should be mapped to the root form + if (null === $violationPath) { + $scope = $form; + } + + // In general, mapping happens from the root form to the leaf forms + // First, the rules of the root form are applied to determine + // the subsequent descendant. The rules of this descendant are then + // applied to find the next and so on, until we have found the + // most specific form that matches the violation. + + // If any of the forms found in this process is not synchronized, + // mapping is aborted. Non-synchronized forms could not reverse + // transform the value entered by the user, thus any further violations + // caused by the (invalid) reverse transformed value should be + // ignored. + + if (null !== $relativePath) { + // Set the scope to the root of the relative path + // This root will usually be $form. If the path contains + // an unmapped form though, the last unmapped form found + // will be the root of the path. + $scope = $relativePath->getRoot(); + $it = new PropertyPathIterator($relativePath); + + while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) { + $scope = $child; + $it->next(); + $match = true; + } + } + + // This case happens if an error happened in the data under a + // form inheriting its parent data that does not match any of the + // children of that form. + if (null !== $violationPath && !$match) { + // If we could not map the error to anything more specific + // than the root element, map it to the innermost directly + // mapped form of the violation path + // e.g. "children[foo].children[bar].data.baz" + // Here the innermost directly mapped child is "bar" + + $scope = $form; + $it = new ViolationPathIterator($violationPath); + + // Note: acceptsErrors() will always return true for forms inheriting + // their parent data, because these forms can never be non-synchronized + // (they don't do any data transformation on their own) + while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) { + if (!$scope->has($it->current())) { + // Break if we find a reference to a non-existing child + break; + } + + $scope = $scope->get($it->current()); + $it->next(); + } + } + + // Follow dot rules until we have the final target + $mapping = $scope->getConfig()->getOption('error_mapping'); + + while ($this->acceptsErrors($scope) && isset($mapping['.'])) { + $dotRule = new MappingRule($scope, '.', $mapping['.']); + $scope = $dotRule->getTarget(); + $mapping = $scope->getConfig()->getOption('error_mapping'); + } + + // Only add the error if the form is synchronized + if ($this->acceptsErrors($scope)) { + if ($violation->getConstraint() instanceof File && (string) \UPLOAD_ERR_INI_SIZE === $violation->getCode()) { + $errorsTarget = $scope; + + while (null !== $errorsTarget->getParent() && $errorsTarget->getConfig()->getErrorBubbling()) { + $errorsTarget = $errorsTarget->getParent(); + } + + $errors = $errorsTarget->getErrors(); + $errorsTarget->clearErrors(); + + foreach ($errors as $error) { + if (!$error instanceof FileUploadError) { + $errorsTarget->addError($error); + } + } + } + + $message = $violation->getMessage(); + $messageTemplate = $violation->getMessageTemplate(); + + if (str_contains($message, '{{ label }}') || str_contains($messageTemplate, '{{ label }}')) { + $form = $scope; + + do { + $labelFormat = $form->getConfig()->getOption('label_format'); + } while (null === $labelFormat && null !== $form = $form->getParent()); + + if (null !== $labelFormat) { + $label = str_replace( + [ + '%name%', + '%id%', + ], + [ + $scope->getName(), + (string) $scope->getPropertyPath(), + ], + $labelFormat + ); + } else { + $label = $scope->getConfig()->getOption('label'); + } + + if (false !== $label) { + if (null === $label && null !== $this->formRenderer) { + $label = $this->formRenderer->humanize($scope->getName()); + } else { + $label ??= $scope->getName(); + } + + if (null !== $this->translator) { + $form = $scope; + $translationParameters[] = $form->getConfig()->getOption('label_translation_parameters', []); + + do { + $translationDomain = $form->getConfig()->getOption('translation_domain'); + array_unshift( + $translationParameters, + $form->getConfig()->getOption('label_translation_parameters', []) + ); + } while (null === $translationDomain && null !== $form = $form->getParent()); + + $translationParameters = array_merge([], ...$translationParameters); + + $label = $this->translator->trans( + $label, + $translationParameters, + $translationDomain + ); + } + + $message = str_replace('{{ label }}', $label, $message); + $messageTemplate = str_replace('{{ label }}', $label, $messageTemplate); + } + } + + $scope->addError(new FormError( + $message, + $messageTemplate, + $violation->getParameters(), + $violation->getPlural(), + $violation + )); + } + } + + /** + * Tries to match the beginning of the property path at the + * current position against the children of the scope. + * + * If a matching child is found, it is returned. Otherwise + * null is returned. + */ + private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it): ?FormInterface + { + $target = null; + $chunk = ''; + $foundAtIndex = null; + + // Construct mapping rules for the given form + $rules = []; + + foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) { + // Dot rules are considered at the very end + if ('.' !== $propertyPath) { + $rules[] = new MappingRule($form, $propertyPath, $targetPath); + } + } + + $children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false); + + while ($it->valid()) { + if ($it->isIndex()) { + $chunk .= '['.$it->current().']'; + } else { + $chunk .= ('' === $chunk ? '' : '.').$it->current(); + } + + // Test mapping rules as long as we have any + foreach ($rules as $key => $rule) { + /** @var MappingRule $rule */ + + // Mapping rule matches completely, terminate. + if (null !== ($form = $rule->match($chunk))) { + return $form; + } + + // Keep only rules that have $chunk as prefix + if (!$rule->isPrefix($chunk)) { + unset($rules[$key]); + } + } + + /** @var FormInterface $child */ + foreach ($children as $i => $child) { + $childPath = (string) $child->getPropertyPath(); + if ($childPath === $chunk) { + $target = $child; + $foundAtIndex = $it->key(); + } elseif (str_starts_with($childPath, $chunk)) { + continue; + } + + unset($children[$i]); + } + + $it->next(); + } + + if (null !== $foundAtIndex) { + $it->seek($foundAtIndex); + } + + return $target; + } + + /** + * Reconstructs a property path from a violation path and a form tree. + */ + private function reconstructPath(ViolationPath $violationPath, FormInterface $origin): ?RelativePath + { + $propertyPathBuilder = new PropertyPathBuilder($violationPath); + $it = $violationPath->getIterator(); + $scope = $origin; + + // Remember the current index in the builder + $i = 0; + + // Expand elements that map to a form (like "children[address]") + for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) { + if (!$scope->has($it->current())) { + // Scope relates to a form that does not exist + // Bail out + break; + } + + // Process child form + $scope = $scope->get($it->current()); + + if ($scope->getConfig()->getInheritData()) { + // Form inherits its parent data + // Cut the piece out of the property path and proceed + $propertyPathBuilder->remove($i); + } else { + /** @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */ + $propertyPath = $scope->getPropertyPath(); + + if (null === $propertyPath) { + // Property path of a mapped form is null + // Should not happen, bail out + break; + } + + $propertyPathBuilder->replace($i, 1, $propertyPath); + $i += $propertyPath->getLength(); + } + } + + $finalPath = $propertyPathBuilder->getPropertyPath(); + + return null !== $finalPath ? new RelativePath($origin, $finalPath) : null; + } + + private function acceptsErrors(FormInterface $form): bool + { + return $this->allowNonSynchronized || $form->isSynchronized(); + } +} diff --git a/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php new file mode 100644 index 0000000000..a72d41df9e --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * @author Bernhard Schussek + */ +interface ViolationMapperInterface +{ + /** + * Maps a constraint violation to a form in the form tree under + * the given form. + * + * @param bool $allowNonSynchronized Whether to allow mapping to non-synchronized forms + * + * @return void + */ + public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false); +} diff --git a/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPath.php b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPath.php new file mode 100644 index 0000000000..0c2a130cc8 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPath.php @@ -0,0 +1,217 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\Form\Exception\OutOfBoundsException; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class ViolationPath implements \IteratorAggregate, PropertyPathInterface +{ + /** @var list */ + private array $elements = []; + private array $isIndex = []; + private array $mapsForm = []; + private string $pathAsString = ''; + private int $length = 0; + + /** + * Creates a new violation path from a string. + * + * @param string $violationPath The property path of a {@link \Symfony\Component\Validator\ConstraintViolation} object + */ + public function __construct(string $violationPath) + { + $path = new PropertyPath($violationPath); + $elements = $path->getElements(); + $data = false; + + for ($i = 0, $l = \count($elements); $i < $l; ++$i) { + if (!$data) { + // The element "data" has not yet been passed + if ('children' === $elements[$i] && $path->isProperty($i)) { + // Skip element "children" + ++$i; + + // Next element must exist and must be an index + // Otherwise consider this the end of the path + if ($i >= $l || !$path->isIndex($i)) { + break; + } + + // All the following index items (regardless if .children is + // explicitly used) are children and grand-children + for (; $i < $l && $path->isIndex($i); ++$i) { + $this->elements[] = $elements[$i]; + $this->isIndex[] = true; + $this->mapsForm[] = true; + } + + // Rewind the pointer as the last element above didn't match + // (even if the pointer was moved forward) + --$i; + } elseif ('data' === $elements[$i] && $path->isProperty($i)) { + // Skip element "data" + ++$i; + + // End of path + if ($i >= $l) { + break; + } + + $this->elements[] = $elements[$i]; + $this->isIndex[] = $path->isIndex($i); + $this->mapsForm[] = false; + $data = true; + } else { + // Neither "children" nor "data" property found + // Consider this the end of the path + break; + } + } else { + // Already after the "data" element + // Pick everything as is + $this->elements[] = $elements[$i]; + $this->isIndex[] = $path->isIndex($i); + $this->mapsForm[] = false; + } + } + + $this->length = \count($this->elements); + + $this->buildString(); + } + + public function __toString(): string + { + return $this->pathAsString; + } + + public function getLength(): int + { + return $this->length; + } + + public function getParent(): ?PropertyPathInterface + { + if ($this->length <= 1) { + return null; + } + + $parent = clone $this; + + --$parent->length; + array_pop($parent->elements); + array_pop($parent->isIndex); + array_pop($parent->mapsForm); + + $parent->buildString(); + + return $parent; + } + + public function getElements(): array + { + return $this->elements; + } + + public function getElement(int $index): string + { + if (!isset($this->elements[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); + } + + return $this->elements[$index]; + } + + public function isProperty(int $index): bool + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); + } + + return !$this->isIndex[$index]; + } + + public function isIndex(int $index): bool + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); + } + + return $this->isIndex[$index]; + } + + public function isNullSafe(int $index): bool + { + return false; + } + + /** + * Returns whether an element maps directly to a form. + * + * Consider the following violation path: + * + * children[address].children[office].data.street + * + * In this example, "address" and "office" map to forms, while + * "street does not. + * + * @throws OutOfBoundsException if the offset is invalid + */ + public function mapsForm(int $index): bool + { + if (!isset($this->mapsForm[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); + } + + return $this->mapsForm[$index]; + } + + /** + * Returns a new iterator for this path. + */ + public function getIterator(): ViolationPathIterator + { + return new ViolationPathIterator($this); + } + + /** + * Builds the string representation from the elements. + */ + private function buildString(): void + { + $this->pathAsString = ''; + $data = false; + + foreach ($this->elements as $index => $element) { + if ($this->mapsForm[$index]) { + $this->pathAsString .= ".children[$element]"; + } elseif (!$data) { + $this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element"); + $data = true; + } else { + $this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element"; + } + } + + if ('' !== $this->pathAsString) { + // remove leading dot + $this->pathAsString = substr($this->pathAsString, 1); + } + } +} diff --git a/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPathIterator.php b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPathIterator.php new file mode 100644 index 0000000000..ed363a7b15 --- /dev/null +++ b/lib/symfony/form/Extension/Validator/ViolationMapper/ViolationPathIterator.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\PropertyAccess\PropertyPathIterator; + +/** + * @author Bernhard Schussek + */ +class ViolationPathIterator extends PropertyPathIterator +{ + public function __construct(ViolationPath $violationPath) + { + parent::__construct($violationPath); + } + + /** + * @return bool + */ + public function mapsForm() + { + return $this->path->mapsForm($this->key()); + } +} diff --git a/lib/symfony/form/FileUploadError.php b/lib/symfony/form/FileUploadError.php new file mode 100644 index 0000000000..20142b2033 --- /dev/null +++ b/lib/symfony/form/FileUploadError.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @internal + */ +class FileUploadError extends FormError +{ +} diff --git a/lib/symfony/form/Form.php b/lib/symfony/form/Form.php new file mode 100644 index 0000000000..76f371e8b7 --- /dev/null +++ b/lib/symfony/form/Form.php @@ -0,0 +1,1022 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Event\PostSetDataEvent; +use Symfony\Component\Form\Event\PostSubmitEvent; +use Symfony\Component\Form\Event\PreSetDataEvent; +use Symfony\Component\Form\Event\PreSubmitEvent; +use Symfony\Component\Form\Event\SubmitEvent; +use Symfony\Component\Form\Exception\AlreadySubmittedException; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Exception\OutOfBoundsException; +use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\Form\Util\InheritDataAwareIterator; +use Symfony\Component\Form\Util\OrderedHashMap; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * Form represents a form. + * + * To implement your own form fields, you need to have a thorough understanding + * of the data flow within a form. A form stores its data in three different + * representations: + * + * (1) the "model" format required by the form's object + * (2) the "normalized" format for internal processing + * (3) the "view" format used for display simple fields + * or map children model data for compound fields + * + * A date field, for example, may store a date as "Y-m-d" string (1) in the + * object. To facilitate processing in the field, this value is normalized + * to a DateTime object (2). In the HTML representation of your form, a + * localized string (3) may be presented to and modified by the user, or it could be an array of values + * to be mapped to choices fields. + * + * In most cases, format (1) and format (2) will be the same. For example, + * a checkbox field uses a Boolean value for both internal processing and + * storage in the object. In these cases you need to set a view transformer + * to convert between formats (2) and (3). You can do this by calling + * addViewTransformer(). + * + * In some cases though it makes sense to make format (1) configurable. To + * demonstrate this, let's extend our above date field to store the value + * either as "Y-m-d" string or as timestamp. Internally we still want to + * use a DateTime object for processing. To convert the data from string/integer + * to DateTime you can set a model transformer by calling + * addModelTransformer(). The normalized data is then converted to the displayed + * data as described before. + * + * The conversions (1) -> (2) -> (3) use the transform methods of the transformers. + * The conversions (3) -> (2) -> (1) use the reverseTransform methods of the transformers. + * + * @author Fabien Potencier + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface +{ + private FormConfigInterface $config; + private ?FormInterface $parent = null; + + /** + * A map of FormInterface instances. + * + * @var OrderedHashMap + */ + private OrderedHashMap $children; + + /** + * @var FormError[] + */ + private array $errors = []; + + private bool $submitted = false; + + /** + * The button that was used to submit the form. + */ + private FormInterface|ClickableInterface|null $clickedButton = null; + + private mixed $modelData = null; + private mixed $normData = null; + private mixed $viewData = null; + + /** + * The submitted values that don't belong to any children. + */ + private array $extraData = []; + + /** + * The transformation failure generated during submission, if any. + */ + private ?TransformationFailedException $transformationFailure = null; + + /** + * Whether the form's data has been initialized. + * + * When the data is initialized with its default value, that default value + * is passed through the transformer chain in order to synchronize the + * model, normalized and view format for the first time. This is done + * lazily in order to save performance when {@link setData()} is called + * manually, making the initialization with the configured default value + * superfluous. + */ + private bool $defaultDataSet = false; + + /** + * Whether setData() is currently being called. + */ + private bool $lockSetData = false; + + private string $name = ''; + + /** + * Whether the form inherits its underlying data from its parent. + */ + private bool $inheritData; + + private ?PropertyPathInterface $propertyPath = null; + + /** + * @throws LogicException if a data mapper is not provided for a compound form + */ + public function __construct(FormConfigInterface $config) + { + // Compound forms always need a data mapper, otherwise calls to + // `setData` and `add` will not lead to the correct population of + // the child forms. + if ($config->getCompound() && !$config->getDataMapper()) { + throw new LogicException('Compound forms need a data mapper.'); + } + + // If the form inherits the data from its parent, it is not necessary + // to call setData() with the default data. + if ($this->inheritData = $config->getInheritData()) { + $this->defaultDataSet = true; + } + + $this->config = $config; + $this->children = new OrderedHashMap(); + $this->name = $config->getName(); + } + + public function __clone() + { + $this->children = clone $this->children; + + foreach ($this->children as $key => $child) { + $this->children[$key] = clone $child; + } + } + + public function getConfig(): FormConfigInterface + { + return $this->config; + } + + public function getName(): string + { + return $this->name; + } + + public function getPropertyPath(): ?PropertyPathInterface + { + if ($this->propertyPath || $this->propertyPath = $this->config->getPropertyPath()) { + return $this->propertyPath; + } + + if ('' === $this->name) { + return null; + } + + $parent = $this->parent; + + while ($parent?->getConfig()->getInheritData()) { + $parent = $parent->getParent(); + } + + if ($parent && null === $parent->getConfig()->getDataClass()) { + $this->propertyPath = new PropertyPath('['.$this->name.']'); + } else { + $this->propertyPath = new PropertyPath($this->name); + } + + return $this->propertyPath; + } + + public function isRequired(): bool + { + if (null === $this->parent || $this->parent->isRequired()) { + return $this->config->getRequired(); + } + + return false; + } + + public function isDisabled(): bool + { + if (null === $this->parent || !$this->parent->isDisabled()) { + return $this->config->getDisabled(); + } + + return true; + } + + public function setParent(?FormInterface $parent = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/form', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + + if ($this->submitted) { + throw new AlreadySubmittedException('You cannot set the parent of a submitted form.'); + } + + if (null !== $parent && '' === $this->name) { + throw new LogicException('A form with an empty name cannot have a parent form.'); + } + + $this->parent = $parent; + + return $this; + } + + public function getParent(): ?FormInterface + { + return $this->parent; + } + + public function getRoot(): FormInterface + { + return $this->parent ? $this->parent->getRoot() : $this; + } + + public function isRoot(): bool + { + return null === $this->parent; + } + + public function setData(mixed $modelData): static + { + // If the form is submitted while disabled, it is set to submitted, but the data is not + // changed. In such cases (i.e. when the form is not initialized yet) don't + // abort this method. + if ($this->submitted && $this->defaultDataSet) { + throw new AlreadySubmittedException('You cannot change the data of a submitted form.'); + } + + // If the form inherits its parent's data, disallow data setting to + // prevent merge conflicts + if ($this->inheritData) { + throw new RuntimeException('You cannot change the data of a form inheriting its parent data.'); + } + + // Don't allow modifications of the configured data if the data is locked + if ($this->config->getDataLocked() && $modelData !== $this->config->getData()) { + return $this; + } + + if (\is_object($modelData) && !$this->config->getByReference()) { + $modelData = clone $modelData; + } + + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead.'); + } + + $this->lockSetData = true; + $dispatcher = $this->config->getEventDispatcher(); + + // Hook to change content of the model data before transformation and mapping children + if ($dispatcher->hasListeners(FormEvents::PRE_SET_DATA)) { + $event = new PreSetDataEvent($this, $modelData); + $dispatcher->dispatch($event, FormEvents::PRE_SET_DATA); + $modelData = $event->getData(); + } + + // Treat data as strings unless a transformer exists + if (\is_scalar($modelData) && !$this->config->getViewTransformers() && !$this->config->getModelTransformers()) { + $modelData = (string) $modelData; + } + + // Synchronize representations - must not change the content! + // Transformation exceptions are not caught on initialization + $normData = $this->modelToNorm($modelData); + $viewData = $this->normToView($normData); + + // Validate if view data matches data class (unless empty) + if (!FormUtil::isEmpty($viewData)) { + $dataClass = $this->config->getDataClass(); + + if (null !== $dataClass && !$viewData instanceof $dataClass) { + $actualType = get_debug_type($viewData); + + throw new LogicException('The form\'s view data is expected to be a "'.$dataClass.'", but it is a "'.$actualType.'". You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms "'.$actualType.'" to an instance of "'.$dataClass.'".'); + } + } + + $this->modelData = $modelData; + $this->normData = $normData; + $this->viewData = $viewData; + $this->defaultDataSet = true; + $this->lockSetData = false; + + // Compound forms don't need to invoke this method if they don't have children + if (\count($this->children) > 0) { + // Update child forms from the data (unless their config data is locked) + $this->config->getDataMapper()->mapDataToForms($viewData, new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children))); + } + + if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) { + $event = new PostSetDataEvent($this, $modelData); + $dispatcher->dispatch($event, FormEvents::POST_SET_DATA); + } + + return $this; + } + + public function getData(): mixed + { + if ($this->inheritData) { + if (!$this->parent) { + throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.'); + } + + return $this->parent->getData(); + } + + if (!$this->defaultDataSet) { + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getData() if the form data has not already been set. You should call getData() on the FormEvent object instead.'); + } + + $this->setData($this->config->getData()); + } + + return $this->modelData; + } + + public function getNormData(): mixed + { + if ($this->inheritData) { + if (!$this->parent) { + throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.'); + } + + return $this->parent->getNormData(); + } + + if (!$this->defaultDataSet) { + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getNormData() if the form data has not already been set.'); + } + + $this->setData($this->config->getData()); + } + + return $this->normData; + } + + public function getViewData(): mixed + { + if ($this->inheritData) { + if (!$this->parent) { + throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.'); + } + + return $this->parent->getViewData(); + } + + if (!$this->defaultDataSet) { + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getViewData() if the form data has not already been set.'); + } + + $this->setData($this->config->getData()); + } + + return $this->viewData; + } + + public function getExtraData(): array + { + return $this->extraData; + } + + public function initialize(): static + { + if (null !== $this->parent) { + throw new RuntimeException('Only root forms should be initialized.'); + } + + // Guarantee that the *_SET_DATA events have been triggered once the + // form is initialized. This makes sure that dynamically added or + // removed fields are already visible after initialization. + if (!$this->defaultDataSet) { + $this->setData($this->config->getData()); + } + + return $this; + } + + public function handleRequest(mixed $request = null): static + { + $this->config->getRequestHandler()->handleRequest($this, $request); + + return $this; + } + + public function submit(mixed $submittedData, bool $clearMissing = true): static + { + if ($this->submitted) { + throw new AlreadySubmittedException('A form can only be submitted once.'); + } + + // Initialize errors in the very beginning so we're sure + // they are collectable during submission only + $this->errors = []; + + // Obviously, a disabled form should not change its data upon submission. + if ($this->isDisabled()) { + $this->submitted = true; + + return $this; + } + + // The data must be initialized if it was not initialized yet. + // This is necessary to guarantee that the *_SET_DATA listeners + // are always invoked before submit() takes place. + if (!$this->defaultDataSet) { + $this->setData($this->config->getData()); + } + + // Treat false as NULL to support binding false to checkboxes. + // Don't convert NULL to a string here in order to determine later + // whether an empty value has been submitted or whether no value has + // been submitted at all. This is important for processing checkboxes + // and radio buttons with empty values. + if (false === $submittedData) { + $submittedData = null; + } elseif (\is_scalar($submittedData)) { + $submittedData = (string) $submittedData; + } elseif ($this->config->getRequestHandler()->isFileUpload($submittedData)) { + if (!$this->config->getOption('allow_file_upload')) { + $submittedData = null; + $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, file upload given.'); + } + } elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->getOption('multiple', false)) { + $submittedData = null; + $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, array given.'); + } + + $dispatcher = $this->config->getEventDispatcher(); + + $modelData = null; + $normData = null; + $viewData = null; + + try { + if (null !== $this->transformationFailure) { + throw $this->transformationFailure; + } + + // Hook to change content of the data submitted by the browser + if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) { + $event = new PreSubmitEvent($this, $submittedData); + $dispatcher->dispatch($event, FormEvents::PRE_SUBMIT); + $submittedData = $event->getData(); + } + + // Check whether the form is compound. + // This check is preferable over checking the number of children, + // since forms without children may also be compound. + // (think of empty collection forms) + if ($this->config->getCompound()) { + if (!\is_array($submittedData ??= [])) { + throw new TransformationFailedException('Compound forms expect an array or NULL on submission.'); + } + + foreach ($this->children as $name => $child) { + $isSubmitted = \array_key_exists($name, $submittedData); + + if ($isSubmitted || $clearMissing) { + $child->submit($isSubmitted ? $submittedData[$name] : null, $clearMissing); + unset($submittedData[$name]); + + if (null !== $this->clickedButton) { + continue; + } + + if ($child instanceof ClickableInterface && $child->isClicked()) { + $this->clickedButton = $child; + + continue; + } + + if (method_exists($child, 'getClickedButton') && null !== $child->getClickedButton()) { + $this->clickedButton = $child->getClickedButton(); + } + } + } + + $this->extraData = $submittedData; + } + + // Forms that inherit their parents' data also are not processed, + // because then it would be too difficult to merge the changes in + // the child and the parent form. Instead, the parent form also takes + // changes in the grandchildren (i.e. children of the form that inherits + // its parent's data) into account. + // (see InheritDataAwareIterator below) + if (!$this->inheritData) { + // If the form is compound, the view data is merged with the data + // of the children using the data mapper. + // If the form is not compound, the view data is assigned to the submitted data. + $viewData = $this->config->getCompound() ? $this->viewData : $submittedData; + + if (FormUtil::isEmpty($viewData)) { + $emptyData = $this->config->getEmptyData(); + + if ($emptyData instanceof \Closure) { + $emptyData = $emptyData($this, $viewData); + } + + $viewData = $emptyData; + } + + // Merge form data from children into existing view data + // It is not necessary to invoke this method if the form has no children, + // even if it is compound. + if (\count($this->children) > 0) { + // Use InheritDataAwareIterator to process children of + // descendants that inherit this form's data. + // These descendants will not be submitted normally (see the check + // for $this->config->getInheritData() above) + $this->config->getDataMapper()->mapFormsToData( + new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)), + $viewData + ); + } + + // Normalize data to unified representation + $normData = $this->viewToNorm($viewData); + + // Hook to change content of the data in the normalized + // representation + if ($dispatcher->hasListeners(FormEvents::SUBMIT)) { + $event = new SubmitEvent($this, $normData); + $dispatcher->dispatch($event, FormEvents::SUBMIT); + $normData = $event->getData(); + } + + // Synchronize representations - must not change the content! + $modelData = $this->normToModel($normData); + $viewData = $this->normToView($normData); + } + } catch (TransformationFailedException $e) { + $this->transformationFailure = $e; + + // If $viewData was not yet set, set it to $submittedData so that + // the erroneous data is accessible on the form. + // Forms that inherit data never set any data, because the getters + // forward to the parent form's getters anyway. + if (null === $viewData && !$this->inheritData) { + $viewData = $submittedData; + } + } + + $this->submitted = true; + $this->modelData = $modelData; + $this->normData = $normData; + $this->viewData = $viewData; + + if ($dispatcher->hasListeners(FormEvents::POST_SUBMIT)) { + $event = new PostSubmitEvent($this, $viewData); + $dispatcher->dispatch($event, FormEvents::POST_SUBMIT); + } + + return $this; + } + + public function addError(FormError $error): static + { + if (null === $error->getOrigin()) { + $error->setOrigin($this); + } + + if ($this->parent && $this->config->getErrorBubbling()) { + $this->parent->addError($error); + } else { + $this->errors[] = $error; + } + + return $this; + } + + public function isSubmitted(): bool + { + return $this->submitted; + } + + public function isSynchronized(): bool + { + return null === $this->transformationFailure; + } + + public function getTransformationFailure(): ?TransformationFailedException + { + return $this->transformationFailure; + } + + public function isEmpty(): bool + { + foreach ($this->children as $child) { + if (!$child->isEmpty()) { + return false; + } + } + + if (null !== $isEmptyCallback = $this->config->getIsEmptyCallback()) { + return $isEmptyCallback($this->modelData); + } + + return FormUtil::isEmpty($this->modelData) + // arrays, countables + || (is_countable($this->modelData) && 0 === \count($this->modelData)) + // traversables that are not countable + || ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData)); + } + + public function isValid(): bool + { + if (!$this->submitted) { + throw new LogicException('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() and ensure that it\'s true before calling Form::isValid().'); + } + + if ($this->isDisabled()) { + return true; + } + + return 0 === \count($this->getErrors(true)); + } + + /** + * Returns the button that was used to submit the form. + */ + public function getClickedButton(): FormInterface|ClickableInterface|null + { + if ($this->clickedButton) { + return $this->clickedButton; + } + + return $this->parent && method_exists($this->parent, 'getClickedButton') ? $this->parent->getClickedButton() : null; + } + + public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator + { + $errors = $this->errors; + + // Copy the errors of nested forms to the $errors array + if ($deep) { + foreach ($this as $child) { + /** @var FormInterface $child */ + if ($child->isSubmitted() && $child->isValid()) { + continue; + } + + $iterator = $child->getErrors(true, $flatten); + + if (0 === \count($iterator)) { + continue; + } + + if ($flatten) { + foreach ($iterator as $error) { + $errors[] = $error; + } + } else { + $errors[] = $iterator; + } + } + } + + return new FormErrorIterator($this, $errors); + } + + public function clearErrors(bool $deep = false): static + { + $this->errors = []; + + if ($deep) { + // Clear errors from children + foreach ($this as $child) { + if ($child instanceof ClearableErrorsInterface) { + $child->clearErrors(true); + } + } + } + + return $this; + } + + public function all(): array + { + return iterator_to_array($this->children); + } + + public function add(FormInterface|string $child, ?string $type = null, array $options = []): static + { + if ($this->submitted) { + throw new AlreadySubmittedException('You cannot add children to a submitted form.'); + } + + if (!$this->config->getCompound()) { + throw new LogicException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?'); + } + + if (!$child instanceof FormInterface) { + if (!\is_string($child) && !\is_int($child)) { + throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormInterface'); + } + + $child = (string) $child; + + // Never initialize child forms automatically + $options['auto_initialize'] = false; + + if (null === $type && null === $this->config->getDataClass()) { + $type = TextType::class; + } + + if (null === $type) { + $child = $this->config->getFormFactory()->createForProperty($this->config->getDataClass(), $child, null, $options); + } else { + $child = $this->config->getFormFactory()->createNamed($child, $type, null, $options); + } + } elseif ($child->getConfig()->getAutoInitialize()) { + throw new RuntimeException(\sprintf('Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "%s".', $child->getName())); + } + + $this->children[$child->getName()] = $child; + + $child->setParent($this); + + // If setData() is currently being called, there is no need to call + // mapDataToForms() here, as mapDataToForms() is called at the end + // of setData() anyway. Not doing this check leads to an endless + // recursion when initializing the form lazily and an event listener + // (such as ResizeFormListener) adds fields depending on the data: + // + // * setData() is called, the form is not initialized yet + // * add() is called by the listener (setData() is not complete, so + // the form is still not initialized) + // * getViewData() is called + // * setData() is called since the form is not initialized yet + // * ... endless recursion ... + // + // Also skip data mapping if setData() has not been called yet. + // setData() will be called upon form initialization and data mapping + // will take place by then. + if (!$this->lockSetData && $this->defaultDataSet && !$this->inheritData) { + $viewData = $this->getViewData(); + $this->config->getDataMapper()->mapDataToForms( + $viewData, + new \RecursiveIteratorIterator(new InheritDataAwareIterator(new \ArrayIterator([$child->getName() => $child]))) + ); + } + + return $this; + } + + public function remove(string $name): static + { + if ($this->submitted) { + throw new AlreadySubmittedException('You cannot remove children from a submitted form.'); + } + + if (isset($this->children[$name])) { + if (!$this->children[$name]->isSubmitted()) { + $this->children[$name]->setParent(null); + } + + unset($this->children[$name]); + } + + return $this; + } + + public function has(string $name): bool + { + return isset($this->children[$name]); + } + + public function get(string $name): FormInterface + { + if (isset($this->children[$name])) { + return $this->children[$name]; + } + + throw new OutOfBoundsException(\sprintf('Child "%s" does not exist.', $name)); + } + + /** + * Returns whether a child with the given name exists (implements the \ArrayAccess interface). + * + * @param string $name The name of the child + */ + public function offsetExists(mixed $name): bool + { + return $this->has($name); + } + + /** + * Returns the child with the given name (implements the \ArrayAccess interface). + * + * @param string $name The name of the child + * + * @throws OutOfBoundsException if the named child does not exist + */ + public function offsetGet(mixed $name): FormInterface + { + return $this->get($name); + } + + /** + * Adds a child to the form (implements the \ArrayAccess interface). + * + * @param string $name Ignored. The name of the child is used + * @param FormInterface $child The child to be added + * + * @throws AlreadySubmittedException if the form has already been submitted + * @throws LogicException when trying to add a child to a non-compound form + * + * @see self::add() + */ + public function offsetSet(mixed $name, mixed $child): void + { + $this->add($child); + } + + /** + * Removes the child with the given name from the form (implements the \ArrayAccess interface). + * + * @param string $name The name of the child to remove + * + * @throws AlreadySubmittedException if the form has already been submitted + */ + public function offsetUnset(mixed $name): void + { + $this->remove($name); + } + + /** + * Returns the iterator for this group. + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + return $this->children; + } + + /** + * Returns the number of form children (implements the \Countable interface). + */ + public function count(): int + { + return \count($this->children); + } + + public function createView(?FormView $parent = null): FormView + { + if (null === $parent && $this->parent) { + $parent = $this->parent->createView(); + } + + $type = $this->config->getType(); + $options = $this->config->getOptions(); + + // The methods createView(), buildView() and finishView() are called + // explicitly here in order to be able to override either of them + // in a custom resolved form type. + $view = $type->createView($this, $parent); + + $type->buildView($view, $this, $options); + + foreach ($this->children as $name => $child) { + $view->children[$name] = $child->createView($view); + } + + $this->sort($view->children); + + $type->finishView($view, $this, $options); + + return $view; + } + + /** + * Sorts view fields based on their priority value. + */ + private function sort(array &$children): void + { + $c = []; + $i = 0; + $needsSorting = false; + foreach ($children as $name => $child) { + $c[$name] = ['p' => $child->vars['priority'] ?? 0, 'i' => $i++]; + + if (0 !== $c[$name]['p']) { + $needsSorting = true; + } + } + + if (!$needsSorting) { + return; + } + + uksort($children, static fn ($a, $b): int => [$c[$b]['p'], $c[$a]['i']] <=> [$c[$a]['p'], $c[$b]['i']]); + } + + /** + * Normalizes the underlying data if a model transformer is set. + * + * @throws TransformationFailedException If the underlying data cannot be transformed to "normalized" format + */ + private function modelToNorm(mixed $value): mixed + { + try { + foreach ($this->config->getModelTransformers() as $transformer) { + $value = $transformer->transform($value); + } + } catch (TransformationFailedException $exception) { + throw new TransformationFailedException(\sprintf('Unable to transform data for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + } + + return $value; + } + + /** + * Reverse transforms a value if a model transformer is set. + * + * @throws TransformationFailedException If the value cannot be transformed to "model" format + */ + private function normToModel(mixed $value): mixed + { + try { + $transformers = $this->config->getModelTransformers(); + + for ($i = \count($transformers) - 1; $i >= 0; --$i) { + $value = $transformers[$i]->reverseTransform($value); + } + } catch (TransformationFailedException $exception) { + throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + } + + return $value; + } + + /** + * Transforms the value if a view transformer is set. + * + * @throws TransformationFailedException If the normalized value cannot be transformed to "view" format + */ + private function normToView(mixed $value): mixed + { + // Scalar values should be converted to strings to + // facilitate differentiation between empty ("") and zero (0). + // Only do this for simple forms, as the resulting value in + // compound forms is passed to the data mapper and thus should + // not be converted to a string before. + if (!($transformers = $this->config->getViewTransformers()) && !$this->config->getCompound()) { + return null === $value || \is_scalar($value) ? (string) $value : $value; + } + + try { + foreach ($transformers as $transformer) { + $value = $transformer->transform($value); + } + } catch (TransformationFailedException $exception) { + throw new TransformationFailedException(\sprintf('Unable to transform value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + } + + return $value; + } + + /** + * Reverse transforms a value if a view transformer is set. + * + * @throws TransformationFailedException If the submitted value cannot be transformed to "normalized" format + */ + private function viewToNorm(mixed $value): mixed + { + if (!$transformers = $this->config->getViewTransformers()) { + return '' === $value ? null : $value; + } + + try { + for ($i = \count($transformers) - 1; $i >= 0; --$i) { + $value = $transformers[$i]->reverseTransform($value); + } + } catch (TransformationFailedException $exception) { + throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + } + + return $value; + } +} diff --git a/lib/symfony/form/FormBuilder.php b/lib/symfony/form/FormBuilder.php new file mode 100644 index 0000000000..3c0f4e2179 --- /dev/null +++ b/lib/symfony/form/FormBuilder.php @@ -0,0 +1,217 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\Type\TextType; + +/** + * A builder for creating {@link Form} instances. + * + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormBuilderInterface +{ + /** + * The children of the form builder. + * + * @var FormBuilderInterface[] + */ + private array $children = []; + + /** + * The data of children who haven't been converted to form builders yet. + */ + private array $unresolvedChildren = []; + + public function __construct(?string $name, ?string $dataClass, EventDispatcherInterface $dispatcher, FormFactoryInterface $factory, array $options = []) + { + parent::__construct($name, $dataClass, $dispatcher, $options); + + $this->setFormFactory($factory); + } + + public function add(FormBuilderInterface|string $child, ?string $type = null, array $options = []): static + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if ($child instanceof FormBuilderInterface) { + $this->children[$child->getName()] = $child; + + // In case an unresolved child with the same name exists + unset($this->unresolvedChildren[$child->getName()]); + + return $this; + } + + if (!\is_string($child) && !\is_int($child)) { + throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormBuilderInterface'); + } + + // Add to "children" to maintain order + $this->children[$child] = null; + $this->unresolvedChildren[$child] = [$type, $options]; + + return $this; + } + + public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if (null === $type && null === $this->getDataClass()) { + $type = TextType::class; + } + + if (null !== $type) { + return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options); + } + + return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options); + } + + public function get(string $name): FormBuilderInterface + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if (isset($this->unresolvedChildren[$name])) { + return $this->resolveChild($name); + } + + if (isset($this->children[$name])) { + return $this->children[$name]; + } + + throw new InvalidArgumentException(\sprintf('The child with the name "%s" does not exist.', $name)); + } + + public function remove(string $name): static + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + unset($this->unresolvedChildren[$name], $this->children[$name]); + + return $this; + } + + public function has(string $name): bool + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + return isset($this->unresolvedChildren[$name]) || isset($this->children[$name]); + } + + public function all(): array + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->resolveChildren(); + + return $this->children; + } + + public function count(): int + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + return \count($this->children); + } + + public function getFormConfig(): FormConfigInterface + { + /** @var self $config */ + $config = parent::getFormConfig(); + + $config->children = []; + $config->unresolvedChildren = []; + + return $config; + } + + public function getForm(): FormInterface + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->resolveChildren(); + + $form = new Form($this->getFormConfig()); + + foreach ($this->children as $child) { + // Automatic initialization is only supported on root forms + $form->add($child->setAutoInitialize(false)->getForm()); + } + + if ($this->getAutoInitialize()) { + // Automatically initialize the form if it is configured so + $form->initialize(); + } + + return $form; + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + if ($this->locked) { + throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + return new \ArrayIterator($this->all()); + } + + /** + * Converts an unresolved child into a {@link FormBuilderInterface} instance. + */ + private function resolveChild(string $name): FormBuilderInterface + { + [$type, $options] = $this->unresolvedChildren[$name]; + + unset($this->unresolvedChildren[$name]); + + return $this->children[$name] = $this->create($name, $type, $options); + } + + /** + * Converts all unresolved children into {@link FormBuilder} instances. + */ + private function resolveChildren(): void + { + foreach ($this->unresolvedChildren as $name => $info) { + $this->children[$name] = $this->create($name, $info[0], $info[1]); + } + + $this->unresolvedChildren = []; + } +} diff --git a/lib/symfony/form/FormBuilderInterface.php b/lib/symfony/form/FormBuilderInterface.php new file mode 100644 index 0000000000..08d29303c9 --- /dev/null +++ b/lib/symfony/form/FormBuilderInterface.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @author Bernhard Schussek + * + * @extends \Traversable + */ +interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface +{ + /** + * Adds a new field to this group. A field must have a unique name within + * the group. Otherwise the existing field is overwritten. + * + * If you add a nested group, this group should also be represented in the + * object hierarchy. + * + * @param array $options + */ + public function add(string|self $child, ?string $type = null, array $options = []): static; + + /** + * Creates a form builder. + * + * @param string $name The name of the form or the name of the property + * @param string|null $type The type of the form or null if name is a property + * @param array $options + */ + public function create(string $name, ?string $type = null, array $options = []): self; + + /** + * Returns a child by name. + * + * @throws Exception\InvalidArgumentException if the given child does not exist + */ + public function get(string $name): self; + + /** + * Removes the field with the given name. + */ + public function remove(string $name): static; + + /** + * Returns whether a field with the given name exists. + */ + public function has(string $name): bool; + + /** + * Returns the children. + * + * @return array + */ + public function all(): array; + + /** + * Creates the form. + */ + public function getForm(): FormInterface; +} diff --git a/lib/symfony/form/FormConfigBuilder.php b/lib/symfony/form/FormConfigBuilder.php new file mode 100644 index 0000000000..a3ec8c48de --- /dev/null +++ b/lib/symfony/form/FormConfigBuilder.php @@ -0,0 +1,657 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\ImmutableEventDispatcher; +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * A basic form configuration. + * + * @author Bernhard Schussek + */ +class FormConfigBuilder implements FormConfigBuilderInterface +{ + /** + * Caches a globally unique {@link NativeRequestHandler} instance. + */ + private static NativeRequestHandler $nativeRequestHandler; + + /** @var bool */ + protected $locked = false; + + private EventDispatcherInterface $dispatcher; + private string $name; + private ?PropertyPathInterface $propertyPath = null; + private bool $mapped = true; + private bool $byReference = true; + private bool $inheritData = false; + private bool $compound = false; + private ResolvedFormTypeInterface $type; + private array $viewTransformers = []; + private array $modelTransformers = []; + private ?DataMapperInterface $dataMapper = null; + private bool $required = true; + private bool $disabled = false; + private bool $errorBubbling = false; + private mixed $emptyData = null; + private array $attributes = []; + private mixed $data = null; + private ?string $dataClass; + private bool $dataLocked = false; + private FormFactoryInterface $formFactory; + private string $action = ''; + private string $method = 'POST'; + private RequestHandlerInterface $requestHandler; + private bool $autoInitialize = false; + private array $options; + private ?\Closure $isEmptyCallback = null; + + /** + * Creates an empty form configuration. + * + * @param string|null $name The form name + * @param string|null $dataClass The class of the form's data + * + * @throws InvalidArgumentException if the data class is not a valid class or if + * the name contains invalid characters + */ + public function __construct(?string $name, ?string $dataClass, EventDispatcherInterface $dispatcher, array $options = []) + { + self::validateName($name); + + if (null !== $dataClass && !class_exists($dataClass) && !interface_exists($dataClass, false)) { + throw new InvalidArgumentException(\sprintf('Class "%s" not found. Is the "data_class" form option set correctly?', $dataClass)); + } + + $this->name = (string) $name; + $this->dataClass = $dataClass; + $this->dispatcher = $dispatcher; + $this->options = $options; + } + + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->dispatcher->addListener($eventName, $listener, $priority); + + return $this; + } + + public function addEventSubscriber(EventSubscriberInterface $subscriber): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->dispatcher->addSubscriber($subscriber); + + return $this; + } + + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if ($forcePrepend) { + array_unshift($this->viewTransformers, $viewTransformer); + } else { + $this->viewTransformers[] = $viewTransformer; + } + + return $this; + } + + public function resetViewTransformers(): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->viewTransformers = []; + + return $this; + } + + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if ($forceAppend) { + $this->modelTransformers[] = $modelTransformer; + } else { + array_unshift($this->modelTransformers, $modelTransformer); + } + + return $this; + } + + public function resetModelTransformers(): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->modelTransformers = []; + + return $this; + } + + public function getEventDispatcher(): EventDispatcherInterface + { + if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) { + $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher); + } + + return $this->dispatcher; + } + + public function getName(): string + { + return $this->name; + } + + public function getPropertyPath(): ?PropertyPathInterface + { + return $this->propertyPath; + } + + public function getMapped(): bool + { + return $this->mapped; + } + + public function getByReference(): bool + { + return $this->byReference; + } + + public function getInheritData(): bool + { + return $this->inheritData; + } + + public function getCompound(): bool + { + return $this->compound; + } + + public function getType(): ResolvedFormTypeInterface + { + return $this->type; + } + + public function getViewTransformers(): array + { + return $this->viewTransformers; + } + + public function getModelTransformers(): array + { + return $this->modelTransformers; + } + + public function getDataMapper(): ?DataMapperInterface + { + return $this->dataMapper; + } + + public function getRequired(): bool + { + return $this->required; + } + + public function getDisabled(): bool + { + return $this->disabled; + } + + public function getErrorBubbling(): bool + { + return $this->errorBubbling; + } + + public function getEmptyData(): mixed + { + return $this->emptyData; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function hasAttribute(string $name): bool + { + return \array_key_exists($name, $this->attributes); + } + + public function getAttribute(string $name, mixed $default = null): mixed + { + return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + public function getData(): mixed + { + return $this->data; + } + + public function getDataClass(): ?string + { + return $this->dataClass; + } + + public function getDataLocked(): bool + { + return $this->dataLocked; + } + + public function getFormFactory(): FormFactoryInterface + { + if (!isset($this->formFactory)) { + throw new BadMethodCallException('The form factory must be set before retrieving it.'); + } + + return $this->formFactory; + } + + public function getAction(): string + { + return $this->action; + } + + public function getMethod(): string + { + return $this->method; + } + + public function getRequestHandler(): RequestHandlerInterface + { + return $this->requestHandler ??= self::$nativeRequestHandler ??= new NativeRequestHandler(); + } + + public function getAutoInitialize(): bool + { + return $this->autoInitialize; + } + + public function getOptions(): array + { + return $this->options; + } + + public function hasOption(string $name): bool + { + return \array_key_exists($name, $this->options); + } + + public function getOption(string $name, mixed $default = null): mixed + { + return \array_key_exists($name, $this->options) ? $this->options[$name] : $default; + } + + public function getIsEmptyCallback(): ?callable + { + return $this->isEmptyCallback; + } + + /** + * @return $this + */ + public function setAttribute(string $name, mixed $value): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->attributes[$name] = $value; + + return $this; + } + + /** + * @return $this + */ + public function setAttributes(array $attributes): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->attributes = $attributes; + + return $this; + } + + /** + * @return $this + */ + public function setDataMapper(?DataMapperInterface $dataMapper = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/form', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->dataMapper = $dataMapper; + + return $this; + } + + /** + * @return $this + */ + public function setDisabled(bool $disabled): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->disabled = $disabled; + + return $this; + } + + /** + * @return $this + */ + public function setEmptyData(mixed $emptyData): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->emptyData = $emptyData; + + return $this; + } + + /** + * @return $this + */ + public function setErrorBubbling(bool $errorBubbling): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->errorBubbling = $errorBubbling; + + return $this; + } + + /** + * @return $this + */ + public function setRequired(bool $required): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->required = $required; + + return $this; + } + + /** + * @return $this + */ + public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if (null !== $propertyPath && !$propertyPath instanceof PropertyPathInterface) { + $propertyPath = new PropertyPath($propertyPath); + } + + $this->propertyPath = $propertyPath; + + return $this; + } + + /** + * @return $this + */ + public function setMapped(bool $mapped): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->mapped = $mapped; + + return $this; + } + + /** + * @return $this + */ + public function setByReference(bool $byReference): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->byReference = $byReference; + + return $this; + } + + /** + * @return $this + */ + public function setInheritData(bool $inheritData): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->inheritData = $inheritData; + + return $this; + } + + /** + * @return $this + */ + public function setCompound(bool $compound): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->compound = $compound; + + return $this; + } + + /** + * @return $this + */ + public function setType(ResolvedFormTypeInterface $type): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->type = $type; + + return $this; + } + + /** + * @return $this + */ + public function setData(mixed $data): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->data = $data; + + return $this; + } + + /** + * @return $this + */ + public function setDataLocked(bool $locked): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->dataLocked = $locked; + + return $this; + } + + /** + * @return $this + */ + public function setFormFactory(FormFactoryInterface $formFactory) + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->formFactory = $formFactory; + + return $this; + } + + /** + * @return $this + */ + public function setAction(string $action): static + { + if ($this->locked) { + throw new BadMethodCallException('The config builder cannot be modified anymore.'); + } + + $this->action = $action; + + return $this; + } + + /** + * @return $this + */ + public function setMethod(string $method): static + { + if ($this->locked) { + throw new BadMethodCallException('The config builder cannot be modified anymore.'); + } + + $this->method = strtoupper($method); + + return $this; + } + + /** + * @return $this + */ + public function setRequestHandler(RequestHandlerInterface $requestHandler): static + { + if ($this->locked) { + throw new BadMethodCallException('The config builder cannot be modified anymore.'); + } + + $this->requestHandler = $requestHandler; + + return $this; + } + + /** + * @return $this + */ + public function setAutoInitialize(bool $initialize): static + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + $this->autoInitialize = $initialize; + + return $this; + } + + public function getFormConfig(): FormConfigInterface + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + // This method should be idempotent, so clone the builder + $config = clone $this; + $config->locked = true; + + return $config; + } + + /** + * @return $this + */ + public function setIsEmptyCallback(?callable $isEmptyCallback): static + { + $this->isEmptyCallback = null === $isEmptyCallback ? null : $isEmptyCallback(...); + + return $this; + } + + /** + * Validates whether the given variable is a valid form name. + * + * @throws InvalidArgumentException if the name contains invalid characters + * + * @internal + */ + final public static function validateName(?string $name): void + { + if (!self::isValidName($name)) { + throw new InvalidArgumentException(\sprintf('The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', $name)); + } + } + + /** + * Returns whether the given variable contains a valid form name. + * + * A name is accepted if it + * + * * is empty + * * starts with a letter, digit or underscore + * * contains only letters, digits, numbers, underscores ("_"), + * hyphens ("-") and colons (":") + */ + final public static function isValidName(?string $name): bool + { + return '' === $name || null === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name); + } +} diff --git a/lib/symfony/form/FormConfigBuilderInterface.php b/lib/symfony/form/FormConfigBuilderInterface.php new file mode 100644 index 0000000000..09b9149801 --- /dev/null +++ b/lib/symfony/form/FormConfigBuilderInterface.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * @author Bernhard Schussek + */ +interface FormConfigBuilderInterface extends FormConfigInterface +{ + /** + * Adds an event listener to an event on this form. + * + * @param int $priority The priority of the listener. Listeners + * with a higher priority are called before + * listeners with a lower priority. + * + * @return $this + */ + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static; + + /** + * Adds an event subscriber for events on this form. + * + * @return $this + */ + public function addEventSubscriber(EventSubscriberInterface $subscriber): static; + + /** + * Appends / prepends a transformer to the view transformer chain. + * + * The transform method of the transformer is used to convert data from the + * normalized to the view format. + * The reverseTransform method of the transformer is used to convert from the + * view to the normalized format. + * + * @param bool $forcePrepend If set to true, prepend instead of appending + * + * @return $this + */ + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static; + + /** + * Clears the view transformers. + * + * @return $this + */ + public function resetViewTransformers(): static; + + /** + * Prepends / appends a transformer to the normalization transformer chain. + * + * The transform method of the transformer is used to convert data from the + * model to the normalized format. + * The reverseTransform method of the transformer is used to convert from the + * normalized to the model format. + * + * @param bool $forceAppend If set to true, append instead of prepending + * + * @return $this + */ + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static; + + /** + * Clears the normalization transformers. + * + * @return $this + */ + public function resetModelTransformers(): static; + + /** + * Sets the value for an attribute. + * + * @param mixed $value The value of the attribute + * + * @return $this + */ + public function setAttribute(string $name, mixed $value): static; + + /** + * Sets the attributes. + * + * @return $this + */ + public function setAttributes(array $attributes): static; + + /** + * Sets the data mapper used by the form. + * + * @return $this + */ + public function setDataMapper(?DataMapperInterface $dataMapper): static; + + /** + * Sets whether the form is disabled. + * + * @return $this + */ + public function setDisabled(bool $disabled): static; + + /** + * Sets the data used for the client data when no value is submitted. + * + * @param mixed $emptyData The empty data + * + * @return $this + */ + public function setEmptyData(mixed $emptyData): static; + + /** + * Sets whether errors bubble up to the parent. + * + * @return $this + */ + public function setErrorBubbling(bool $errorBubbling): static; + + /** + * Sets whether this field is required to be filled out when submitted. + * + * @return $this + */ + public function setRequired(bool $required): static; + + /** + * Sets the property path that the form should be mapped to. + * + * @param string|PropertyPathInterface|null $propertyPath The property path or null if the path should be set + * automatically based on the form's name + * + * @return $this + */ + public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static; + + /** + * Sets whether the form should be mapped to an element of its + * parent's data. + * + * @return $this + */ + public function setMapped(bool $mapped): static; + + /** + * Sets whether the form's data should be modified by reference. + * + * @return $this + */ + public function setByReference(bool $byReference): static; + + /** + * Sets whether the form should read and write the data of its parent. + * + * @return $this + */ + public function setInheritData(bool $inheritData): static; + + /** + * Sets whether the form should be compound. + * + * @return $this + * + * @see FormConfigInterface::getCompound() + */ + public function setCompound(bool $compound): static; + + /** + * Sets the resolved type. + * + * @return $this + */ + public function setType(ResolvedFormTypeInterface $type): static; + + /** + * Sets the initial data of the form. + * + * @param mixed $data The data of the form in model format + * + * @return $this + */ + public function setData(mixed $data): static; + + /** + * Locks the form's data to the data passed in the configuration. + * + * A form with locked data is restricted to the data passed in + * this configuration. The data can only be modified then by + * submitting the form or using PRE_SET_DATA event. + * + * It means data passed to a factory method or mapped from the + * parent will be ignored. + * + * @return $this + */ + public function setDataLocked(bool $locked): static; + + /** + * Sets the form factory used for creating new forms. + * + * @return $this + */ + public function setFormFactory(FormFactoryInterface $formFactory); + + /** + * Sets the target URL of the form. + * + * @return $this + */ + public function setAction(string $action): static; + + /** + * Sets the HTTP method used by the form. + * + * @return $this + */ + public function setMethod(string $method): static; + + /** + * Sets the request handler used by the form. + * + * @return $this + */ + public function setRequestHandler(RequestHandlerInterface $requestHandler): static; + + /** + * Sets whether the form should be initialized automatically. + * + * Should be set to true only for root forms. + * + * @param bool $initialize True to initialize the form automatically, + * false to suppress automatic initialization. + * In the second case, you need to call + * {@link FormInterface::initialize()} manually. + * + * @return $this + */ + public function setAutoInitialize(bool $initialize): static; + + /** + * Builds and returns the form configuration. + */ + public function getFormConfig(): FormConfigInterface; + + /** + * Sets the callback that will be called to determine if the model + * data of the form is empty or not. + * + * @return $this + */ + public function setIsEmptyCallback(?callable $isEmptyCallback): static; +} diff --git a/lib/symfony/form/FormConfigInterface.php b/lib/symfony/form/FormConfigInterface.php new file mode 100644 index 0000000000..93d1998ec2 --- /dev/null +++ b/lib/symfony/form/FormConfigInterface.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * The configuration of a {@link Form} object. + * + * @author Bernhard Schussek + */ +interface FormConfigInterface +{ + /** + * Returns the event dispatcher used to dispatch form events. + */ + public function getEventDispatcher(): EventDispatcherInterface; + + /** + * Returns the name of the form used as HTTP parameter. + */ + public function getName(): string; + + /** + * Returns the property path that the form should be mapped to. + */ + public function getPropertyPath(): ?PropertyPathInterface; + + /** + * Returns whether the form should be mapped to an element of its + * parent's data. + */ + public function getMapped(): bool; + + /** + * Returns whether the form's data should be modified by reference. + */ + public function getByReference(): bool; + + /** + * Returns whether the form should read and write the data of its parent. + */ + public function getInheritData(): bool; + + /** + * Returns whether the form is compound. + * + * This property is independent of whether the form actually has + * children. A form can be compound and have no children at all, like + * for example an empty collection form. + * The contrary is not possible, a form which is not compound + * cannot have any children. + */ + public function getCompound(): bool; + + /** + * Returns the resolved form type used to construct the form. + */ + public function getType(): ResolvedFormTypeInterface; + + /** + * Returns the view transformers of the form. + * + * @return DataTransformerInterface[] + */ + public function getViewTransformers(): array; + + /** + * Returns the model transformers of the form. + * + * @return DataTransformerInterface[] + */ + public function getModelTransformers(): array; + + /** + * Returns the data mapper of the compound form or null for a simple form. + */ + public function getDataMapper(): ?DataMapperInterface; + + /** + * Returns whether the form is required. + */ + public function getRequired(): bool; + + /** + * Returns whether the form is disabled. + */ + public function getDisabled(): bool; + + /** + * Returns whether errors attached to the form will bubble to its parent. + */ + public function getErrorBubbling(): bool; + + /** + * Used when the view data is empty on submission. + * + * When the form is compound it will also be used to map the + * children data. + * + * The empty data must match the view format as it will passed to the first view transformer's + * "reverseTransform" method. + */ + public function getEmptyData(): mixed; + + /** + * Returns additional attributes of the form. + */ + public function getAttributes(): array; + + /** + * Returns whether the attribute with the given name exists. + */ + public function hasAttribute(string $name): bool; + + /** + * Returns the value of the given attribute. + */ + public function getAttribute(string $name, mixed $default = null): mixed; + + /** + * Returns the initial data of the form. + */ + public function getData(): mixed; + + /** + * Returns the class of the view data or null if the data is scalar or an array. + */ + public function getDataClass(): ?string; + + /** + * Returns whether the form's data is locked. + * + * A form with locked data is restricted to the data passed in + * this configuration. The data can only be modified then by + * submitting the form. + */ + public function getDataLocked(): bool; + + /** + * Returns the form factory used for creating new forms. + */ + public function getFormFactory(): FormFactoryInterface; + + /** + * Returns the target URL of the form. + */ + public function getAction(): string; + + /** + * Returns the HTTP method used by the form. + */ + public function getMethod(): string; + + /** + * Returns the request handler used by the form. + */ + public function getRequestHandler(): RequestHandlerInterface; + + /** + * Returns whether the form should be initialized upon creation. + */ + public function getAutoInitialize(): bool; + + /** + * Returns all options passed during the construction of the form. + * + * @return array The passed options + */ + public function getOptions(): array; + + /** + * Returns whether a specific option exists. + */ + public function hasOption(string $name): bool; + + /** + * Returns the value of a specific option. + */ + public function getOption(string $name, mixed $default = null): mixed; + + /** + * Returns a callable that takes the model data as argument and that returns if it is empty or not. + */ + public function getIsEmptyCallback(): ?callable; +} diff --git a/lib/symfony/form/FormError.php b/lib/symfony/form/FormError.php new file mode 100644 index 0000000000..b9b326277d --- /dev/null +++ b/lib/symfony/form/FormError.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\BadMethodCallException; + +/** + * Wraps errors in forms. + * + * @author Bernhard Schussek + */ +class FormError +{ + protected $messageTemplate; + protected $messageParameters; + protected $messagePluralization; + + private string $message; + private mixed $cause; + + /** + * The form that spawned this error. + */ + private ?FormInterface $origin = null; + + /** + * Any array key in $messageParameters will be used as a placeholder in + * $messageTemplate. + * + * @param string $message The translated error message + * @param string|null $messageTemplate The template for the error message + * @param array $messageParameters The parameters that should be + * substituted in the message template + * @param int|null $messagePluralization The value for error message pluralization + * @param mixed $cause The cause of the error + * + * @see \Symfony\Component\Translation\Translator + */ + public function __construct(string $message, ?string $messageTemplate = null, array $messageParameters = [], ?int $messagePluralization = null, mixed $cause = null) + { + $this->message = $message; + $this->messageTemplate = $messageTemplate ?: $message; + $this->messageParameters = $messageParameters; + $this->messagePluralization = $messagePluralization; + $this->cause = $cause; + } + + /** + * Returns the error message. + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Returns the error message template. + */ + public function getMessageTemplate(): string + { + return $this->messageTemplate; + } + + /** + * Returns the parameters to be inserted in the message template. + */ + public function getMessageParameters(): array + { + return $this->messageParameters; + } + + /** + * Returns the value for error message pluralization. + */ + public function getMessagePluralization(): ?int + { + return $this->messagePluralization; + } + + /** + * Returns the cause of this error. + */ + public function getCause(): mixed + { + return $this->cause; + } + + /** + * Sets the form that caused this error. + * + * This method must only be called once. + * + * @return void + * + * @throws BadMethodCallException If the method is called more than once + */ + public function setOrigin(FormInterface $origin) + { + if (null !== $this->origin) { + throw new BadMethodCallException('setOrigin() must only be called once.'); + } + + $this->origin = $origin; + } + + /** + * Returns the form that caused this error. + */ + public function getOrigin(): ?FormInterface + { + return $this->origin; + } +} diff --git a/lib/symfony/form/FormErrorIterator.php b/lib/symfony/form/FormErrorIterator.php new file mode 100644 index 0000000000..e563c9b2de --- /dev/null +++ b/lib/symfony/form/FormErrorIterator.php @@ -0,0 +1,275 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Exception\OutOfBoundsException; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * Iterates over the errors of a form. + * + * This class supports recursive iteration. In order to iterate recursively, + * pass a structure of {@link FormError} and {@link FormErrorIterator} objects + * to the $errors constructor argument. + * + * You can also wrap the iterator into a {@link \RecursiveIteratorIterator} to + * flatten the recursive structure into a flat list of errors. + * + * @author Bernhard Schussek + * + * @template T of FormError|FormErrorIterator + * + * @implements \ArrayAccess + * @implements \RecursiveIterator + * @implements \SeekableIterator + */ +class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable, \Stringable +{ + /** + * The prefix used for indenting nested error messages. + */ + public const INDENTATION = ' '; + + private FormInterface $form; + + /** + * @var list + */ + private array $errors; + + /** + * @param list $errors + * + * @throws InvalidArgumentException If the errors are invalid + */ + public function __construct(FormInterface $form, array $errors) + { + foreach ($errors as $error) { + if (!($error instanceof FormError || $error instanceof self)) { + throw new InvalidArgumentException(\sprintf('The errors must be instances of "Symfony\Component\Form\FormError" or "%s". Got: "%s".', __CLASS__, get_debug_type($error))); + } + } + + $this->form = $form; + $this->errors = $errors; + } + + /** + * Returns all iterated error messages as string. + */ + public function __toString(): string + { + $string = ''; + + foreach ($this->errors as $error) { + if ($error instanceof FormError) { + $string .= 'ERROR: '.$error->getMessage()."\n"; + } else { + /** @var self $error */ + $string .= $error->getForm()->getName().":\n"; + $string .= self::indent((string) $error); + } + } + + return $string; + } + + /** + * Returns the iterated form. + */ + public function getForm(): FormInterface + { + return $this->form; + } + + /** + * Returns the current element of the iterator. + * + * @return T An error or an iterator containing nested errors + */ + public function current(): FormError|self + { + return current($this->errors); + } + + /** + * Advances the iterator to the next position. + */ + public function next(): void + { + next($this->errors); + } + + /** + * Returns the current position of the iterator. + */ + public function key(): int + { + return key($this->errors); + } + + /** + * Returns whether the iterator's position is valid. + */ + public function valid(): bool + { + return null !== key($this->errors); + } + + /** + * Sets the iterator's position to the beginning. + * + * This method detects if errors have been added to the form since the + * construction of the iterator. + */ + public function rewind(): void + { + reset($this->errors); + } + + /** + * Returns whether a position exists in the iterator. + * + * @param int $position The position + */ + public function offsetExists(mixed $position): bool + { + return isset($this->errors[$position]); + } + + /** + * Returns the element at a position in the iterator. + * + * @param int $position The position + * + * @return T + * + * @throws OutOfBoundsException If the given position does not exist + */ + public function offsetGet(mixed $position): FormError|self + { + if (!isset($this->errors[$position])) { + throw new OutOfBoundsException('The offset '.$position.' does not exist.'); + } + + return $this->errors[$position]; + } + + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function offsetSet(mixed $position, mixed $value): void + { + throw new BadMethodCallException('The iterator doesn\'t support modification of elements.'); + } + + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function offsetUnset(mixed $position): void + { + throw new BadMethodCallException('The iterator doesn\'t support modification of elements.'); + } + + /** + * Returns whether the current element of the iterator can be recursed + * into. + */ + public function hasChildren(): bool + { + return current($this->errors) instanceof self; + } + + public function getChildren(): self + { + if (!$this->hasChildren()) { + throw new LogicException(\sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()')); + } + + /** @var self $children */ + $children = current($this->errors); + + return $children; + } + + /** + * Returns the number of elements in the iterator. + * + * Note that this is not the total number of errors, if the constructor + * parameter $deep was set to true! In that case, you should wrap the + * iterator into a {@link \RecursiveIteratorIterator} with the standard mode + * {@link \RecursiveIteratorIterator::LEAVES_ONLY} and count the result. + * + * $iterator = new \RecursiveIteratorIterator($form->getErrors(true)); + * $count = count(iterator_to_array($iterator)); + * + * Alternatively, set the constructor argument $flatten to true as well. + * + * $count = count($form->getErrors(true, true)); + */ + public function count(): int + { + return \count($this->errors); + } + + /** + * Sets the position of the iterator. + * + * @throws OutOfBoundsException If the position is invalid + */ + public function seek(int $position): void + { + if (!isset($this->errors[$position])) { + throw new OutOfBoundsException('The offset '.$position.' does not exist.'); + } + + reset($this->errors); + + while ($position !== key($this->errors)) { + next($this->errors); + } + } + + /** + * Creates iterator for errors with specific codes. + * + * @param string|string[] $codes The codes to find + */ + public function findByCodes(string|array $codes): static + { + $codes = (array) $codes; + $errors = []; + foreach ($this as $error) { + $cause = $error->getCause(); + if ($cause instanceof ConstraintViolation && \in_array($cause->getCode(), $codes, true)) { + $errors[] = $error; + } + } + + return new static($this->form, $errors); + } + + /** + * Utility function for indenting multi-line strings. + */ + private static function indent(string $string): string + { + return rtrim(self::INDENTATION.str_replace("\n", "\n".self::INDENTATION, $string), ' '); + } +} diff --git a/lib/symfony/form/FormEvent.php b/lib/symfony/form/FormEvent.php new file mode 100644 index 0000000000..1e6aa34d63 --- /dev/null +++ b/lib/symfony/form/FormEvent.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author Bernhard Schussek + */ +class FormEvent extends Event +{ + private FormInterface $form; + protected $data; + + public function __construct(FormInterface $form, mixed $data) + { + $this->form = $form; + $this->data = $data; + } + + /** + * Returns the form at the source of the event. + */ + public function getForm(): FormInterface + { + return $this->form; + } + + /** + * Returns the data associated with this event. + */ + public function getData(): mixed + { + return $this->data; + } + + /** + * Allows updating with some filtered data. + * + * @return void + */ + public function setData(mixed $data) + { + $this->data = $data; + } +} diff --git a/lib/symfony/form/FormEvents.php b/lib/symfony/form/FormEvents.php new file mode 100644 index 0000000000..cf4d97f556 --- /dev/null +++ b/lib/symfony/form/FormEvents.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Event\PostSetDataEvent; +use Symfony\Component\Form\Event\PostSubmitEvent; +use Symfony\Component\Form\Event\PreSetDataEvent; +use Symfony\Component\Form\Event\PreSubmitEvent; +use Symfony\Component\Form\Event\SubmitEvent; + +/** + * To learn more about how form events work check the documentation + * entry at {@link https://symfony.com/doc/any/components/form/form_events.html}. + * + * To learn how to dynamically modify forms using events check the cookbook + * entry at {@link https://symfony.com/doc/any/cookbook/form/dynamic_form_modification.html}. + * + * @author Bernhard Schussek + */ +final class FormEvents +{ + /** + * The PRE_SUBMIT event is dispatched at the beginning of the Form::submit() method. + * + * It can be used to: + * - Change data from the request, before submitting the data to the form. + * - Add or remove form fields, before submitting the data to the form. + * + * @Event("Symfony\Component\Form\Event\PreSubmitEvent") + */ + public const PRE_SUBMIT = 'form.pre_submit'; + + /** + * The SUBMIT event is dispatched after the Form::submit() method + * has changed the view data by the request data, or submitted and mapped + * the children if the form is compound, and after reverse transformation + * to normalized representation. + * + * It's also dispatched just before the Form::submit() method transforms back + * the normalized data to the model and view data. + * + * So at this stage children of compound forms are submitted and synchronized, unless + * their transformation failed, but a parent would still be at the PRE_SUBMIT level. + * + * Since the current form is not synchronized yet, it is still possible to add and + * remove fields. + * + * @Event("Symfony\Component\Form\Event\SubmitEvent") + */ + public const SUBMIT = 'form.submit'; + + /** + * The FormEvents::POST_SUBMIT event is dispatched at the very end of the Form::submit(). + * + * It this stage the model and view data may have been denormalized. Otherwise the form + * is desynchronized because transformation failed during submission. + * + * It can be used to fetch data after denormalization. + * + * The event attaches the current view data. To know whether this is the renormalized data + * or the invalid request data, call Form::isSynchronized() first. + * + * @Event("Symfony\Component\Form\Event\PostSubmitEvent") + */ + public const POST_SUBMIT = 'form.post_submit'; + + /** + * The FormEvents::PRE_SET_DATA event is dispatched at the beginning of the Form::setData() method. + * + * It can be used to: + * - Modify the data given during pre-population; + * - Keep synchronized the form depending on the data (adding or removing fields dynamically). + * + * @Event("Symfony\Component\Form\Event\PreSetDataEvent") + */ + public const PRE_SET_DATA = 'form.pre_set_data'; + + /** + * The FormEvents::POST_SET_DATA event is dispatched at the end of the Form::setData() method. + * + * This event can be used to modify the form depending on the final state of the underlying data + * accessible in every representation: model, normalized and view. + * + * @Event("Symfony\Component\Form\Event\PostSetDataEvent") + */ + public const POST_SET_DATA = 'form.post_set_data'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + public const ALIASES = [ + PreSubmitEvent::class => self::PRE_SUBMIT, + SubmitEvent::class => self::SUBMIT, + PostSubmitEvent::class => self::POST_SUBMIT, + PreSetDataEvent::class => self::PRE_SET_DATA, + PostSetDataEvent::class => self::POST_SET_DATA, + ]; + + private function __construct() + { + } +} diff --git a/lib/symfony/form/FormExtensionInterface.php b/lib/symfony/form/FormExtensionInterface.php new file mode 100644 index 0000000000..e540e18256 --- /dev/null +++ b/lib/symfony/form/FormExtensionInterface.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * Interface for extensions which provide types, type extensions and a guesser. + */ +interface FormExtensionInterface +{ + /** + * Returns a type by name. + * + * @param string $name The name of the type + * + * @throws Exception\InvalidArgumentException if the given type is not supported by this extension + */ + public function getType(string $name): FormTypeInterface; + + /** + * Returns whether the given type is supported. + * + * @param string $name The name of the type + */ + public function hasType(string $name): bool; + + /** + * Returns the extensions for the given type. + * + * @param string $name The name of the type + * + * @return FormTypeExtensionInterface[] + */ + public function getTypeExtensions(string $name): array; + + /** + * Returns whether this extension provides type extensions for the given type. + * + * @param string $name The name of the type + */ + public function hasTypeExtensions(string $name): bool; + + /** + * Returns the type guesser provided by this extension. + */ + public function getTypeGuesser(): ?FormTypeGuesserInterface; +} diff --git a/lib/symfony/form/FormFactory.php b/lib/symfony/form/FormFactory.php new file mode 100644 index 0000000000..9e1234f831 --- /dev/null +++ b/lib/symfony/form/FormFactory.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; + +class FormFactory implements FormFactoryInterface +{ + private FormRegistryInterface $registry; + + public function __construct(FormRegistryInterface $registry) + { + $this->registry = $registry; + } + + public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface + { + return $this->createBuilder($type, $data, $options)->getForm(); + } + + public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface + { + return $this->createNamedBuilder($name, $type, $data, $options)->getForm(); + } + + public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface + { + return $this->createBuilderForProperty($class, $property, $data, $options)->getForm(); + } + + public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface + { + return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $data, $options); + } + + public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface + { + if (null !== $data && !\array_key_exists('data', $options)) { + $options['data'] = $data; + } + + $type = $this->registry->getType($type); + + $builder = $type->createBuilder($this, $name, $options); + + // Explicitly call buildForm() in order to be able to override either + // createBuilder() or buildForm() in the resolved form type + $type->buildForm($builder, $builder->getOptions()); + + return $builder; + } + + public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface + { + if (null === $guesser = $this->registry->getTypeGuesser()) { + return $this->createNamedBuilder($property, TextType::class, $data, $options); + } + + $typeGuess = $guesser->guessType($class, $property); + $maxLengthGuess = $guesser->guessMaxLength($class, $property); + $requiredGuess = $guesser->guessRequired($class, $property); + $patternGuess = $guesser->guessPattern($class, $property); + + $type = $typeGuess ? $typeGuess->getType() : TextType::class; + + $maxLength = $maxLengthGuess?->getValue(); + $pattern = $patternGuess?->getValue(); + + if (null !== $pattern) { + $options = array_replace_recursive(['attr' => ['pattern' => $pattern]], $options); + } + + if (null !== $maxLength) { + $options = array_replace_recursive(['attr' => ['maxlength' => $maxLength]], $options); + } + + if ($requiredGuess) { + $options = array_merge(['required' => $requiredGuess->getValue()], $options); + } + + // user options may override guessed options + if ($typeGuess) { + $attrs = []; + $typeGuessOptions = $typeGuess->getOptions(); + if (isset($typeGuessOptions['attr']) && isset($options['attr'])) { + $attrs = ['attr' => array_merge($typeGuessOptions['attr'], $options['attr'])]; + } + + $options = array_merge($typeGuessOptions, $options, $attrs); + } + + return $this->createNamedBuilder($property, $type, $data, $options); + } +} diff --git a/lib/symfony/form/FormFactoryBuilder.php b/lib/symfony/form/FormFactoryBuilder.php new file mode 100644 index 0000000000..42b8dec9f4 --- /dev/null +++ b/lib/symfony/form/FormFactoryBuilder.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Extension\Core\CoreExtension; + +/** + * The default implementation of FormFactoryBuilderInterface. + * + * @author Bernhard Schussek + */ +class FormFactoryBuilder implements FormFactoryBuilderInterface +{ + private bool $forceCoreExtension; + + private ResolvedFormTypeFactoryInterface $resolvedTypeFactory; + + /** + * @var FormExtensionInterface[] + */ + private array $extensions = []; + + /** + * @var FormTypeInterface[] + */ + private array $types = []; + + /** + * @var FormTypeExtensionInterface[][] + */ + private array $typeExtensions = []; + + /** + * @var FormTypeGuesserInterface[] + */ + private array $typeGuessers = []; + + public function __construct(bool $forceCoreExtension = false) + { + $this->forceCoreExtension = $forceCoreExtension; + } + + public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static + { + $this->resolvedTypeFactory = $resolvedTypeFactory; + + return $this; + } + + public function addExtension(FormExtensionInterface $extension): static + { + $this->extensions[] = $extension; + + return $this; + } + + public function addExtensions(array $extensions): static + { + $this->extensions = array_merge($this->extensions, $extensions); + + return $this; + } + + public function addType(FormTypeInterface $type): static + { + $this->types[] = $type; + + return $this; + } + + public function addTypes(array $types): static + { + foreach ($types as $type) { + $this->types[] = $type; + } + + return $this; + } + + public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static + { + foreach ($typeExtension::getExtendedTypes() as $extendedType) { + $this->typeExtensions[$extendedType][] = $typeExtension; + } + + return $this; + } + + public function addTypeExtensions(array $typeExtensions): static + { + foreach ($typeExtensions as $typeExtension) { + $this->addTypeExtension($typeExtension); + } + + return $this; + } + + public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static + { + $this->typeGuessers[] = $typeGuesser; + + return $this; + } + + public function addTypeGuessers(array $typeGuessers): static + { + $this->typeGuessers = array_merge($this->typeGuessers, $typeGuessers); + + return $this; + } + + public function getFormFactory(): FormFactoryInterface + { + $extensions = $this->extensions; + + if ($this->forceCoreExtension) { + $hasCoreExtension = false; + + foreach ($extensions as $extension) { + if ($extension instanceof CoreExtension) { + $hasCoreExtension = true; + break; + } + } + + if (!$hasCoreExtension) { + array_unshift($extensions, new CoreExtension()); + } + } + + if (\count($this->types) > 0 || \count($this->typeExtensions) > 0 || \count($this->typeGuessers) > 0) { + if (\count($this->typeGuessers) > 1) { + $typeGuesser = new FormTypeGuesserChain($this->typeGuessers); + } else { + $typeGuesser = $this->typeGuessers[0] ?? null; + } + + $extensions[] = new PreloadedExtension($this->types, $this->typeExtensions, $typeGuesser); + } + + $registry = new FormRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedFormTypeFactory()); + + return new FormFactory($registry); + } +} diff --git a/lib/symfony/form/FormFactoryBuilderInterface.php b/lib/symfony/form/FormFactoryBuilderInterface.php new file mode 100644 index 0000000000..70bdf507b3 --- /dev/null +++ b/lib/symfony/form/FormFactoryBuilderInterface.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\Form; + +/** + * A builder for FormFactoryInterface objects. + * + * @author Bernhard Schussek + */ +interface FormFactoryBuilderInterface +{ + /** + * Sets the factory for creating ResolvedFormTypeInterface instances. + * + * @return $this + */ + public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static; + + /** + * Adds an extension to be loaded by the factory. + * + * @return $this + */ + public function addExtension(FormExtensionInterface $extension): static; + + /** + * Adds a list of extensions to be loaded by the factory. + * + * @param FormExtensionInterface[] $extensions The extensions + * + * @return $this + */ + public function addExtensions(array $extensions): static; + + /** + * Adds a form type to the factory. + * + * @return $this + */ + public function addType(FormTypeInterface $type): static; + + /** + * Adds a list of form types to the factory. + * + * @param FormTypeInterface[] $types The form types + * + * @return $this + */ + public function addTypes(array $types): static; + + /** + * Adds a form type extension to the factory. + * + * @return $this + */ + public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static; + + /** + * Adds a list of form type extensions to the factory. + * + * @param FormTypeExtensionInterface[] $typeExtensions The form type extensions + * + * @return $this + */ + public function addTypeExtensions(array $typeExtensions): static; + + /** + * Adds a type guesser to the factory. + * + * @return $this + */ + public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static; + + /** + * Adds a list of type guessers to the factory. + * + * @param FormTypeGuesserInterface[] $typeGuessers The type guessers + * + * @return $this + */ + public function addTypeGuessers(array $typeGuessers): static; + + /** + * Builds and returns the factory. + */ + public function getFormFactory(): FormFactoryInterface; +} diff --git a/lib/symfony/form/FormFactoryInterface.php b/lib/symfony/form/FormFactoryInterface.php new file mode 100644 index 0000000000..0f311c0e57 --- /dev/null +++ b/lib/symfony/form/FormFactoryInterface.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; + +/** + * Allows creating a form based on a name, a class or a property. + * + * @author Bernhard Schussek + */ +interface FormFactoryInterface +{ + /** + * Returns a form. + * + * @see createBuilder() + * + * @param mixed $data The initial data + * + * @throws InvalidOptionsException if any given option is not applicable to the given type + */ + public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface; + + /** + * Returns a form. + * + * @see createNamedBuilder() + * + * @param mixed $data The initial data + * + * @throws InvalidOptionsException if any given option is not applicable to the given type + */ + public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface; + + /** + * Returns a form for a property of a class. + * + * @see createBuilderForProperty() + * + * @param string $class The fully qualified class name + * @param string $property The name of the property to guess for + * @param mixed $data The initial data + * + * @throws InvalidOptionsException if any given option is not applicable to the form type + */ + public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface; + + /** + * Returns a form builder. + * + * @param mixed $data The initial data + * + * @throws InvalidOptionsException if any given option is not applicable to the given type + */ + public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface; + + /** + * Returns a form builder. + * + * @param mixed $data The initial data + * + * @throws InvalidOptionsException if any given option is not applicable to the given type + */ + public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface; + + /** + * Returns a form builder for a property of a class. + * + * If any of the 'required' and type options can be guessed, + * and are not provided in the options argument, the guessed value is used. + * + * @param string $class The fully qualified class name + * @param string $property The name of the property to guess for + * @param mixed $data The initial data + * + * @throws InvalidOptionsException if any given option is not applicable to the form type + */ + public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface; +} diff --git a/lib/symfony/form/FormInterface.php b/lib/symfony/form/FormInterface.php new file mode 100644 index 0000000000..23392c4931 --- /dev/null +++ b/lib/symfony/form/FormInterface.php @@ -0,0 +1,289 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * A form group bundling multiple forms in a hierarchical structure. + * + * @author Bernhard Schussek + * + * @extends \ArrayAccess + * @extends \Traversable + */ +interface FormInterface extends \ArrayAccess, \Traversable, \Countable +{ + /** + * Sets the parent form. + * + * @param FormInterface|null $parent The parent form or null if it's the root + * + * @return $this + * + * @throws Exception\AlreadySubmittedException if the form has already been submitted + * @throws Exception\LogicException when trying to set a parent for a form with + * an empty name + */ + public function setParent(?self $parent): static; + + /** + * Returns the parent form. + */ + public function getParent(): ?self; + + /** + * Adds or replaces a child to the form. + * + * @param FormInterface|string $child The FormInterface instance or the name of the child + * @param string|null $type The child's type, if a name was passed + * @param array $options The child's options, if a name was passed + * + * @return $this + * + * @throws Exception\AlreadySubmittedException if the form has already been submitted + * @throws Exception\LogicException when trying to add a child to a non-compound form + * @throws Exception\UnexpectedTypeException if $child or $type has an unexpected type + */ + public function add(self|string $child, ?string $type = null, array $options = []): static; + + /** + * Returns the child with the given name. + * + * @throws Exception\OutOfBoundsException if the named child does not exist + */ + public function get(string $name): self; + + /** + * Returns whether a child with the given name exists. + */ + public function has(string $name): bool; + + /** + * Removes a child from the form. + * + * @return $this + * + * @throws Exception\AlreadySubmittedException if the form has already been submitted + */ + public function remove(string $name): static; + + /** + * Returns all children in this group. + * + * @return self[] + */ + public function all(): array; + + /** + * Returns the errors of this form. + * + * @param bool $deep Whether to include errors of child forms as well + * @param bool $flatten Whether to flatten the list of errors in case + * $deep is set to true + */ + public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator; + + /** + * Updates the form with default model data. + * + * @param mixed $modelData The data formatted as expected for the underlying object + * + * @return $this + * + * @throws Exception\AlreadySubmittedException If the form has already been submitted + * @throws Exception\LogicException if the view data does not match the expected type + * according to {@link FormConfigInterface::getDataClass} + * @throws Exception\RuntimeException If listeners try to call setData in a cycle or if + * the form inherits data from its parent + * @throws Exception\TransformationFailedException if the synchronization failed + */ + public function setData(mixed $modelData): static; + + /** + * Returns the model data in the format needed for the underlying object. + * + * @return mixed When the field is not submitted, the default data is returned. + * When the field is submitted, the default data has been bound + * to the submitted view data. + * + * @throws Exception\RuntimeException If the form inherits data but has no parent + */ + public function getData(): mixed; + + /** + * Returns the normalized data of the field, used as internal bridge + * between model data and view data. + * + * @return mixed When the field is not submitted, the default data is returned. + * When the field is submitted, the normalized submitted data + * is returned if the field is synchronized with the view data, + * null otherwise. + * + * @throws Exception\RuntimeException If the form inherits data but has no parent + */ + public function getNormData(): mixed; + + /** + * Returns the view data of the field. + * + * It may be defined by {@link FormConfigInterface::getDataClass}. + * + * There are two cases: + * + * - When the form is compound the view data is mapped to the children. + * Each child will use its mapped data as model data. + * It can be an array, an object or null. + * + * - When the form is simple its view data is used to be bound + * to the submitted data. + * It can be a string or an array. + * + * In both cases the view data is the actual altered data on submission. + * + * @throws Exception\RuntimeException If the form inherits data but has no parent + */ + public function getViewData(): mixed; + + /** + * Returns the extra submitted data. + * + * @return array The submitted data which do not belong to a child + */ + public function getExtraData(): array; + + /** + * Returns the form's configuration. + */ + public function getConfig(): FormConfigInterface; + + /** + * Returns whether the form is submitted. + */ + public function isSubmitted(): bool; + + /** + * Returns the name by which the form is identified in forms. + * + * Only root forms are allowed to have an empty name. + */ + public function getName(): string; + + /** + * Returns the property path that the form is mapped to. + */ + public function getPropertyPath(): ?PropertyPathInterface; + + /** + * Adds an error to this form. + * + * @return $this + */ + public function addError(FormError $error): static; + + /** + * Returns whether the form and all children are valid. + * + * @throws Exception\LogicException if the form is not submitted + */ + public function isValid(): bool; + + /** + * Returns whether the form is required to be filled out. + * + * If the form has a parent and the parent is not required, this method + * will always return false. Otherwise the value set with setRequired() + * is returned. + */ + public function isRequired(): bool; + + /** + * Returns whether this form is disabled. + * + * The content of a disabled form is displayed, but not allowed to be + * modified. The validation of modified disabled forms should fail. + * + * Forms whose parents are disabled are considered disabled regardless of + * their own state. + */ + public function isDisabled(): bool; + + /** + * Returns whether the form is empty. + */ + public function isEmpty(): bool; + + /** + * Returns whether the data in the different formats is synchronized. + * + * If the data is not synchronized, you can get the transformation failure + * by calling {@link getTransformationFailure()}. + * + * If the form is not submitted, this method always returns true. + */ + public function isSynchronized(): bool; + + /** + * Returns the data transformation failure, if any, during submission. + */ + public function getTransformationFailure(): ?Exception\TransformationFailedException; + + /** + * Initializes the form tree. + * + * Should be called on the root form after constructing the tree. + * + * @return $this + * + * @throws Exception\RuntimeException If the form is not the root + */ + public function initialize(): static; + + /** + * Inspects the given request and calls {@link submit()} if the form was + * submitted. + * + * Internally, the request is forwarded to the configured + * {@link RequestHandlerInterface} instance, which determines whether to + * submit the form or not. + * + * @return $this + */ + public function handleRequest(mixed $request = null): static; + + /** + * Submits data to the form. + * + * @param string|array|null $submittedData The submitted data + * @param bool $clearMissing Whether to set fields to NULL + * when they are missing in the + * submitted data. This argument + * is only used in compound form + * + * @return $this + * + * @throws Exception\AlreadySubmittedException if the form has already been submitted + */ + public function submit(string|array|null $submittedData, bool $clearMissing = true): static; + + /** + * Returns the root of the form tree. + */ + public function getRoot(): self; + + /** + * Returns whether the field is the root of the form tree. + */ + public function isRoot(): bool; + + public function createView(?FormView $parent = null): FormView; +} diff --git a/lib/symfony/form/FormRegistry.php b/lib/symfony/form/FormRegistry.php new file mode 100644 index 0000000000..aafef0dbda --- /dev/null +++ b/lib/symfony/form/FormRegistry.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\ExceptionInterface; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * The central registry of the Form component. + * + * @author Bernhard Schussek + */ +class FormRegistry implements FormRegistryInterface +{ + /** + * @var FormExtensionInterface[] + */ + private array $extensions = []; + + /** + * @var ResolvedFormTypeInterface[] + */ + private array $types = []; + + private FormTypeGuesserInterface|false|null $guesser = false; + private ResolvedFormTypeFactoryInterface $resolvedTypeFactory; + private array $checkedTypes = []; + + /** + * @param FormExtensionInterface[] $extensions + * + * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface + */ + public function __construct(array $extensions, ResolvedFormTypeFactoryInterface $resolvedTypeFactory) + { + foreach ($extensions as $extension) { + if (!$extension instanceof FormExtensionInterface) { + throw new UnexpectedTypeException($extension, FormExtensionInterface::class); + } + } + + $this->extensions = $extensions; + $this->resolvedTypeFactory = $resolvedTypeFactory; + } + + public function getType(string $name): ResolvedFormTypeInterface + { + if (!isset($this->types[$name])) { + $type = null; + + foreach ($this->extensions as $extension) { + if ($extension->hasType($name)) { + $type = $extension->getType($name); + break; + } + } + + if (!$type) { + // Support fully-qualified class names + if (!class_exists($name)) { + throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not exist.', $name)); + } + if (!is_subclass_of($name, FormTypeInterface::class)) { + throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not implement "Symfony\Component\Form\FormTypeInterface".', $name)); + } + + $type = new $name(); + } + + $this->types[$name] = $this->resolveType($type); + } + + return $this->types[$name]; + } + + /** + * Wraps a type into a ResolvedFormTypeInterface implementation and connects it with its parent type. + */ + private function resolveType(FormTypeInterface $type): ResolvedFormTypeInterface + { + $parentType = $type->getParent(); + $fqcn = $type::class; + + if (isset($this->checkedTypes[$fqcn])) { + $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn])); + throw new LogicException(\sprintf('Circular reference detected for form type "%s" (%s).', $fqcn, $types)); + } + + $this->checkedTypes[$fqcn] = true; + + $typeExtensions = []; + try { + foreach ($this->extensions as $extension) { + $typeExtensions[] = $extension->getTypeExtensions($fqcn); + } + + return $this->resolvedTypeFactory->createResolvedType( + $type, + array_merge([], ...$typeExtensions), + $parentType ? $this->getType($parentType) : null + ); + } finally { + unset($this->checkedTypes[$fqcn]); + } + } + + public function hasType(string $name): bool + { + if (isset($this->types[$name])) { + return true; + } + + try { + $this->getType($name); + } catch (ExceptionInterface) { + return false; + } + + return true; + } + + public function getTypeGuesser(): ?FormTypeGuesserInterface + { + if (false === $this->guesser) { + $guessers = []; + + foreach ($this->extensions as $extension) { + $guesser = $extension->getTypeGuesser(); + + if ($guesser) { + $guessers[] = $guesser; + } + } + + $this->guesser = $guessers ? new FormTypeGuesserChain($guessers) : null; + } + + return $this->guesser; + } + + public function getExtensions(): array + { + return $this->extensions; + } +} diff --git a/lib/symfony/form/FormRegistryInterface.php b/lib/symfony/form/FormRegistryInterface.php new file mode 100644 index 0000000000..b1e77898e2 --- /dev/null +++ b/lib/symfony/form/FormRegistryInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * The central registry of the Form component. + * + * @author Bernhard Schussek + */ +interface FormRegistryInterface +{ + /** + * Returns a form type by name. + * + * This methods registers the type extensions from the form extensions. + * + * @throws Exception\InvalidArgumentException if the type cannot be retrieved from any extension + */ + public function getType(string $name): ResolvedFormTypeInterface; + + /** + * Returns whether the given form type is supported. + */ + public function hasType(string $name): bool; + + /** + * Returns the guesser responsible for guessing types. + */ + public function getTypeGuesser(): ?FormTypeGuesserInterface; + + /** + * Returns the extensions loaded by the framework. + * + * @return FormExtensionInterface[] + */ + public function getExtensions(): array; +} diff --git a/lib/symfony/form/FormRenderer.php b/lib/symfony/form/FormRenderer.php new file mode 100644 index 0000000000..ff5e2eb86b --- /dev/null +++ b/lib/symfony/form/FormRenderer.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Twig\Environment; + +/** + * Renders a form into HTML using a rendering engine. + * + * @author Bernhard Schussek + */ +class FormRenderer implements FormRendererInterface +{ + public const CACHE_KEY_VAR = 'unique_block_prefix'; + + private FormRendererEngineInterface $engine; + private ?CsrfTokenManagerInterface $csrfTokenManager; + private array $blockNameHierarchyMap = []; + private array $hierarchyLevelMap = []; + private array $variableStack = []; + + public function __construct(FormRendererEngineInterface $engine, ?CsrfTokenManagerInterface $csrfTokenManager = null) + { + $this->engine = $engine; + $this->csrfTokenManager = $csrfTokenManager; + } + + public function getEngine(): FormRendererEngineInterface + { + return $this->engine; + } + + /** + * @return void + */ + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true) + { + $this->engine->setTheme($view, $themes, $useDefaultThemes); + } + + public function renderCsrfToken(string $tokenId): string + { + if (null === $this->csrfTokenManager) { + throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct(). Try running "composer require symfony/security-csrf".'); + } + + return $this->csrfTokenManager->getToken($tokenId)->getValue(); + } + + public function renderBlock(FormView $view, string $blockName, array $variables = []): string + { + $resource = $this->engine->getResourceForBlockName($view, $blockName); + + if (!$resource) { + throw new LogicException(\sprintf('No block "%s" found while rendering the form.', $blockName)); + } + + $viewCacheKey = $view->vars[self::CACHE_KEY_VAR]; + + // The variables are cached globally for a view (instead of for the + // current suffix) + if (!isset($this->variableStack[$viewCacheKey])) { + $this->variableStack[$viewCacheKey] = []; + + // The default variable scope contains all view variables, merged with + // the variables passed explicitly to the helper + $scopeVariables = $view->vars; + + $varInit = true; + } else { + // Reuse the current scope and merge it with the explicitly passed variables + $scopeVariables = end($this->variableStack[$viewCacheKey]); + + $varInit = false; + } + + // Merge the passed with the existing attributes + if (isset($variables['attr']) && isset($scopeVariables['attr'])) { + $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']); + } + + // Merge the passed with the exist *label* attributes + if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) { + $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']); + } + + // Do not use array_replace_recursive(), otherwise array variables + // cannot be overwritten + $variables = array_replace($scopeVariables, $variables); + + $this->variableStack[$viewCacheKey][] = $variables; + + // Do the rendering + $html = $this->engine->renderBlock($view, $resource, $blockName, $variables); + + // Clear the stack + array_pop($this->variableStack[$viewCacheKey]); + + if ($varInit) { + unset($this->variableStack[$viewCacheKey]); + } + + return $html; + } + + public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string + { + $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix; + + if ($renderOnlyOnce && $view->isRendered()) { + // This is not allowed, because it would result in rendering same IDs multiple times, which is not valid. + throw new BadMethodCallException(\sprintf('Field "%s" has already been rendered, save the result of previous render call to a variable and output that instead.', $view->vars['name'])); + } + + // The cache key for storing the variables and types + $viewCacheKey = $view->vars[self::CACHE_KEY_VAR]; + $viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix; + + // In templates, we have to deal with two kinds of block hierarchies: + // + // +---------+ +---------+ + // | Theme B | -------> | Theme A | + // +---------+ +---------+ + // + // form_widget -------> form_widget + // ^ + // | + // choice_widget -----> choice_widget + // + // The first kind of hierarchy is the theme hierarchy. This allows to + // override the block "choice_widget" from Theme A in the extending + // Theme B. This kind of inheritance needs to be supported by the + // template engine and, for example, offers "parent()" or similar + // functions to fall back from the custom to the parent implementation. + // + // The second kind of hierarchy is the form type hierarchy. This allows + // to implement a custom "choice_widget" block (no matter in which theme), + // or to fallback to the block of the parent type, which would be + // "form_widget" in this example (again, no matter in which theme). + // If the designer wants to explicitly fallback to "form_widget" in their + // custom "choice_widget", for example because they only want to wrap + // a
around the original implementation, they can call the + // widget() function again to render the block for the parent type. + // + // The second kind is implemented in the following blocks. + if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) { + // INITIAL CALL + // Calculate the hierarchy of template blocks and start on + // the bottom level of the hierarchy (= "__
" block) + $blockNameHierarchy = []; + foreach ($view->vars['block_prefixes'] as $blockNamePrefix) { + $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix; + } + $hierarchyLevel = \count($blockNameHierarchy) - 1; + + $hierarchyInit = true; + } else { + // RECURSIVE CALL + // If a block recursively calls searchAndRenderBlock() again, resume rendering + // using the parent type in the hierarchy. + $blockNameHierarchy = $this->blockNameHierarchyMap[$viewAndSuffixCacheKey]; + $hierarchyLevel = $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1; + + $hierarchyInit = false; + } + + // The variables are cached globally for a view (instead of for the + // current suffix) + if (!isset($this->variableStack[$viewCacheKey])) { + $this->variableStack[$viewCacheKey] = []; + + // The default variable scope contains all view variables, merged with + // the variables passed explicitly to the helper + $scopeVariables = $view->vars; + + $varInit = true; + } else { + // Reuse the current scope and merge it with the explicitly passed variables + $scopeVariables = end($this->variableStack[$viewCacheKey]); + + $varInit = false; + } + + // Load the resource where this block can be found + $resource = $this->engine->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $hierarchyLevel); + + // Update the current hierarchy level to the one at which the resource was + // found. For example, if looking for "choice_widget", but only a resource + // is found for its parent "form_widget", then the level is updated here + // to the parent level. + $hierarchyLevel = $this->engine->getResourceHierarchyLevel($view, $blockNameHierarchy, $hierarchyLevel); + + // The actually existing block name in $resource + $blockName = $blockNameHierarchy[$hierarchyLevel]; + + // Escape if no resource exists for this block + if (!$resource) { + if (\count($blockNameHierarchy) !== \count(array_unique($blockNameHierarchy))) { + throw new LogicException(\sprintf('Unable to render the form because the block names array contains duplicates: "%s".', implode('", "', array_reverse($blockNameHierarchy)))); + } + + throw new LogicException(\sprintf('Unable to render the form as none of the following blocks exist: "%s".', implode('", "', array_reverse($blockNameHierarchy)))); + } + + // Merge the passed with the existing attributes + if (isset($variables['attr']) && isset($scopeVariables['attr'])) { + $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']); + } + + // Merge the passed with the exist *label* attributes + if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) { + $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']); + } + + // Do not use array_replace_recursive(), otherwise array variables + // cannot be overwritten + $variables = array_replace($scopeVariables, $variables); + + // In order to make recursive calls possible, we need to store the block hierarchy, + // the current level of the hierarchy and the variables so that this method can + // resume rendering one level higher of the hierarchy when it is called recursively. + // + // We need to store these values in maps (associative arrays) because within a + // call to widget() another call to widget() can be made, but for a different view + // object. These nested calls should not override each other. + $this->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy; + $this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel; + + // We also need to store the variables for the view so that we can render other + // blocks for the same view using the same variables as in the outer block. + $this->variableStack[$viewCacheKey][] = $variables; + + // Do the rendering + $html = $this->engine->renderBlock($view, $resource, $blockName, $variables); + + // Clear the stack + array_pop($this->variableStack[$viewCacheKey]); + + // Clear the caches if they were filled for the first time within + // this function call + if ($hierarchyInit) { + unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey], $this->hierarchyLevelMap[$viewAndSuffixCacheKey]); + } + + if ($varInit) { + unset($this->variableStack[$viewCacheKey]); + } + + if ($renderOnlyOnce) { + $view->setRendered(); + } + + return $html; + } + + public function humanize(string $text): string + { + return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $text)))); + } + + /** + * @internal + */ + public function encodeCurrency(Environment $environment, string $text, string $widget = ''): string + { + if ('UTF-8' === $charset = $environment->getCharset()) { + $text = htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + } else { + $text = htmlentities($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + $text = iconv('UTF-8', $charset, $text); + $widget = iconv('UTF-8', $charset, $widget); + } + + return str_replace('{{ widget }}', $widget, $text); + } +} diff --git a/lib/symfony/form/FormRendererEngineInterface.php b/lib/symfony/form/FormRendererEngineInterface.php new file mode 100644 index 0000000000..e7de3544a1 --- /dev/null +++ b/lib/symfony/form/FormRendererEngineInterface.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * Adapter for rendering form templates with a specific templating engine. + * + * @author Bernhard Schussek + */ +interface FormRendererEngineInterface +{ + /** + * Sets the theme(s) to be used for rendering a view and its children. + * + * @param FormView $view The view to assign the theme(s) to + * @param mixed $themes The theme(s). The type of these themes + * is open to the implementation. + * + * @return void + */ + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true); + + /** + * Returns the resource for a block name. + * + * The resource is first searched in the themes attached to $view, then + * in the themes of its parent view and so on, until a resource was found. + * + * The type of the resource is decided by the implementation. The resource + * is later passed to {@link renderBlock()} by the rendering algorithm. + * + * @param FormView $view The view for determining the used themes. + * First the themes attached directly to the + * view with {@link setTheme()} are considered, + * then the ones of its parent etc. + * + * @return mixed the renderer resource or false, if none was found + */ + public function getResourceForBlockName(FormView $view, string $blockName): mixed; + + /** + * Returns the resource for a block hierarchy. + * + * A block hierarchy is an array which starts with the root of the hierarchy + * and continues with the child of that root, the child of that child etc. + * The following is an example for a block hierarchy: + * + * form_widget + * text_widget + * url_widget + * + * In this example, "url_widget" is the most specific block, while the other + * blocks are its ancestors in the hierarchy. + * + * The second parameter $hierarchyLevel determines the level of the hierarchy + * that should be rendered. For example, if $hierarchyLevel is 2 for the + * above hierarchy, the engine will first look for the block "url_widget", + * then, if that does not exist, for the block "text_widget" etc. + * + * The type of the resource is decided by the implementation. The resource + * is later passed to {@link renderBlock()} by the rendering algorithm. + * + * @param FormView $view The view for determining the used themes. + * First the themes attached directly to + * the view with {@link setTheme()} are + * considered, then the ones of its parent etc. + * @param string[] $blockNameHierarchy The block name hierarchy, with the root block + * at the beginning + * @param int $hierarchyLevel The level in the hierarchy at which to start + * looking. Level 0 indicates the root block, i.e. + * the first element of $blockNameHierarchy. + * + * @return mixed The renderer resource or false, if none was found + */ + public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed; + + /** + * Returns the hierarchy level at which a resource can be found. + * + * A block hierarchy is an array which starts with the root of the hierarchy + * and continues with the child of that root, the child of that child etc. + * The following is an example for a block hierarchy: + * + * form_widget + * text_widget + * url_widget + * + * The second parameter $hierarchyLevel determines the level of the hierarchy + * that should be rendered. + * + * If we call this method with the hierarchy level 2, the engine will first + * look for a resource for block "url_widget". If such a resource exists, + * the method returns 2. Otherwise it tries to find a resource for block + * "text_widget" (at level 1) and, again, returns 1 if a resource was found. + * The method continues to look for resources until the root level was + * reached and nothing was found. In this case false is returned. + * + * The type of the resource is decided by the implementation. The resource + * is later passed to {@link renderBlock()} by the rendering algorithm. + * + * @param FormView $view The view for determining the used themes. + * First the themes attached directly to + * the view with {@link setTheme()} are + * considered, then the ones of its parent etc. + * @param string[] $blockNameHierarchy The block name hierarchy, with the root block + * at the beginning + * @param int $hierarchyLevel The level in the hierarchy at which to start + * looking. Level 0 indicates the root block, i.e. + * the first element of $blockNameHierarchy. + */ + public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false; + + /** + * Renders a block in the given renderer resource. + * + * The resource can be obtained by calling {@link getResourceForBlock()} + * or {@link getResourceForBlockHierarchy()}. The type of the resource is + * decided by the implementation. + * + * @param FormView $view The view to render + * @param mixed $resource The renderer resource + * @param array $variables The variables to pass to the template + * + * @return string + */ + public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []); +} diff --git a/lib/symfony/form/FormRendererInterface.php b/lib/symfony/form/FormRendererInterface.php new file mode 100644 index 0000000000..8e805727ce --- /dev/null +++ b/lib/symfony/form/FormRendererInterface.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * Renders a form into HTML. + * + * @author Bernhard Schussek + */ +interface FormRendererInterface +{ + /** + * Returns the engine used by this renderer. + */ + public function getEngine(): FormRendererEngineInterface; + + /** + * Sets the theme(s) to be used for rendering a view and its children. + * + * @param FormView $view The view to assign the theme(s) to + * @param mixed $themes The theme(s). The type of these themes + * is open to the implementation. + * @param bool $useDefaultThemes If true, will use default themes specified + * in the renderer + * + * @return void + */ + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true); + + /** + * Renders a named block of the form theme. + * + * @param FormView $view The view for which to render the block + * @param array $variables The variables to pass to the template + */ + public function renderBlock(FormView $view, string $blockName, array $variables = []): string; + + /** + * Searches and renders a block for a given name suffix. + * + * The block is searched by combining the block names stored in the + * form view with the given suffix. If a block name is found, that + * block is rendered. + * + * If this method is called recursively, the block search is continued + * where a block was found before. + * + * @param FormView $view The view for which to render the block + * @param array $variables The variables to pass to the template + */ + public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string; + + /** + * Renders a CSRF token. + * + * Use this helper for CSRF protection without the overhead of creating a + * form. + * + * + * + * Check the token in your action using the same token ID. + * + * // $csrfProvider being an instance of Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface + * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) { + * throw new \RuntimeException('CSRF attack detected.'); + * } + */ + public function renderCsrfToken(string $tokenId): string; + + /** + * Makes a technical name human readable. + * + * Sequences of underscores are replaced by single spaces. The first letter + * of the resulting string is capitalized, while all other letters are + * turned to lowercase. + */ + public function humanize(string $text): string; +} diff --git a/lib/symfony/form/FormTypeExtensionInterface.php b/lib/symfony/form/FormTypeExtensionInterface.php new file mode 100644 index 0000000000..ae76457cd6 --- /dev/null +++ b/lib/symfony/form/FormTypeExtensionInterface.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Bernhard Schussek + */ +interface FormTypeExtensionInterface +{ + /** + * Gets the extended types. + * + * @return string[] + */ + public static function getExtendedTypes(): iterable; + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver); + + /** + * Builds the form. + * + * This method is called after the extended type has built the form to + * further modify it. + * + * @param array $options + * + * @return void + * + * @see FormTypeInterface::buildForm() + */ + public function buildForm(FormBuilderInterface $builder, array $options); + + /** + * Builds the view. + * + * This method is called after the extended type has built the view to + * further modify it. + * + * @param array $options + * + * @return void + * + * @see FormTypeInterface::buildView() + */ + public function buildView(FormView $view, FormInterface $form, array $options); + + /** + * Finishes the view. + * + * This method is called after the extended type has finished the view to + * further modify it. + * + * @param array $options + * + * @return void + * + * @see FormTypeInterface::finishView() + */ + public function finishView(FormView $view, FormInterface $form, array $options); +} diff --git a/lib/symfony/form/FormTypeGuesserChain.php b/lib/symfony/form/FormTypeGuesserChain.php new file mode 100644 index 0000000000..ed94ece6e9 --- /dev/null +++ b/lib/symfony/form/FormTypeGuesserChain.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Guess\ValueGuess; + +class FormTypeGuesserChain implements FormTypeGuesserInterface +{ + private array $guessers = []; + + /** + * @param FormTypeGuesserInterface[] $guessers + * + * @throws UnexpectedTypeException if any guesser does not implement FormTypeGuesserInterface + */ + public function __construct(iterable $guessers) + { + $tmpGuessers = []; + foreach ($guessers as $guesser) { + if (!$guesser instanceof FormTypeGuesserInterface) { + throw new UnexpectedTypeException($guesser, FormTypeGuesserInterface::class); + } + + if ($guesser instanceof self) { + $tmpGuessers[] = $guesser->guessers; + } else { + $tmpGuessers[] = [$guesser]; + } + } + + $this->guessers = array_merge([], ...$tmpGuessers); + } + + public function guessType(string $class, string $property): ?TypeGuess + { + return $this->guess(static fn ($guesser) => $guesser->guessType($class, $property)); + } + + public function guessRequired(string $class, string $property): ?ValueGuess + { + return $this->guess(static fn ($guesser) => $guesser->guessRequired($class, $property)); + } + + public function guessMaxLength(string $class, string $property): ?ValueGuess + { + return $this->guess(static fn ($guesser) => $guesser->guessMaxLength($class, $property)); + } + + public function guessPattern(string $class, string $property): ?ValueGuess + { + return $this->guess(static fn ($guesser) => $guesser->guessPattern($class, $property)); + } + + /** + * Executes a closure for each guesser and returns the best guess from the + * return values. + * + * @param \Closure $closure The closure to execute. Accepts a guesser + * as argument and should return a Guess instance + */ + private function guess(\Closure $closure): ?Guess + { + $guesses = []; + + foreach ($this->guessers as $guesser) { + if ($guess = $closure($guesser)) { + $guesses[] = $guess; + } + } + + return Guess::getBestGuess($guesses); + } +} diff --git a/lib/symfony/form/FormTypeGuesserInterface.php b/lib/symfony/form/FormTypeGuesserInterface.php new file mode 100644 index 0000000000..54414b9f69 --- /dev/null +++ b/lib/symfony/form/FormTypeGuesserInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @author Bernhard Schussek + */ +interface FormTypeGuesserInterface +{ + /** + * Returns a field guess for a property name of a class. + * + * @return Guess\TypeGuess|null + */ + public function guessType(string $class, string $property); + + /** + * Returns a guess whether a property of a class is required. + * + * @return Guess\ValueGuess|null + */ + public function guessRequired(string $class, string $property); + + /** + * Returns a guess about the field's maximum length. + * + * @return Guess\ValueGuess|null + */ + public function guessMaxLength(string $class, string $property); + + /** + * Returns a guess about the field's pattern. + * + * @return Guess\ValueGuess|null + */ + public function guessPattern(string $class, string $property); +} diff --git a/lib/symfony/form/FormTypeInterface.php b/lib/symfony/form/FormTypeInterface.php new file mode 100644 index 0000000000..2bc9f7711e --- /dev/null +++ b/lib/symfony/form/FormTypeInterface.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Bernhard Schussek + */ +interface FormTypeInterface +{ + /** + * Returns the name of the parent type. + * + * The parent type and its extensions will configure the form with the + * following methods before the current implementation. + * + * @return string|null + */ + public function getParent(); + + /** + * Configures the options for this type. + * + * @return void + */ + public function configureOptions(OptionsResolver $resolver); + + /** + * Builds the form. + * + * This method is called for each type in the hierarchy starting from the + * top most type. Type extensions can further modify the form. + * + * @param array $options + * + * @return void + * + * @see FormTypeExtensionInterface::buildForm() + */ + public function buildForm(FormBuilderInterface $builder, array $options); + + /** + * Builds the form view. + * + * This method is called for each type in the hierarchy starting from the + * top most type. Type extensions can further modify the view. + * + * A view of a form is built before the views of the child forms are built. + * This means that you cannot access child views in this method. If you need + * to do so, move your logic to {@link finishView()} instead. + * + * @param array $options + * + * @return void + * + * @see FormTypeExtensionInterface::buildView() + */ + public function buildView(FormView $view, FormInterface $form, array $options); + + /** + * Finishes the form view. + * + * This method gets called for each type in the hierarchy starting from the + * top most type. Type extensions can further modify the view. + * + * When this method is called, views of the form's children have already + * been built and finished and can be accessed. You should only implement + * such logic in this method that actually accesses child views. For everything + * else you are recommended to implement {@link buildView()} instead. + * + * @param array $options + * + * @return void + * + * @see FormTypeExtensionInterface::finishView() + */ + public function finishView(FormView $view, FormInterface $form, array $options); + + /** + * Returns the prefix of the template block name for this type. + * + * The block prefix defaults to the underscored short class name with + * the "Type" suffix removed (e.g. "UserProfileType" => "user_profile"). + * + * @return string + */ + public function getBlockPrefix(); +} diff --git a/lib/symfony/form/FormView.php b/lib/symfony/form/FormView.php new file mode 100644 index 0000000000..a6fc1df620 --- /dev/null +++ b/lib/symfony/form/FormView.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\BadMethodCallException; + +/** + * @author Bernhard Schussek + * + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +class FormView implements \ArrayAccess, \IteratorAggregate, \Countable +{ + /** + * The variables assigned to this view. + */ + public $vars = [ + 'value' => null, + 'attr' => [], + ]; + + /** + * The parent view. + */ + public $parent; + + /** + * The child views. + * + * @var array + */ + public $children = []; + + /** + * Is the form attached to this renderer rendered? + * + * Rendering happens when either the widget or the row method was called. + * Row implicitly includes widget, however certain rendering mechanisms + * have to skip widget rendering when a row is rendered. + */ + private bool $rendered = false; + + private bool $methodRendered = false; + + public function __construct(?self $parent = null) + { + $this->parent = $parent; + } + + /** + * Returns whether the view was already rendered. + */ + public function isRendered(): bool + { + if (true === $this->rendered || 0 === \count($this->children)) { + return $this->rendered; + } + + foreach ($this->children as $child) { + if (!$child->isRendered()) { + return false; + } + } + + return $this->rendered = true; + } + + /** + * Marks the view as rendered. + * + * @return $this + */ + public function setRendered(): static + { + $this->rendered = true; + + return $this; + } + + public function isMethodRendered(): bool + { + return $this->methodRendered; + } + + /** + * @return void + */ + public function setMethodRendered() + { + $this->methodRendered = true; + } + + /** + * Returns a child by name (implements \ArrayAccess). + * + * @param int|string $name The child name + */ + public function offsetGet(mixed $name): self + { + return $this->children[$name]; + } + + /** + * Returns whether the given child exists (implements \ArrayAccess). + * + * @param int|string $name The child name + */ + public function offsetExists(mixed $name): bool + { + return isset($this->children[$name]); + } + + /** + * Implements \ArrayAccess. + * + * @throws BadMethodCallException always as setting a child by name is not allowed + */ + public function offsetSet(mixed $name, mixed $value): void + { + throw new BadMethodCallException('Not supported.'); + } + + /** + * Removes a child (implements \ArrayAccess). + * + * @param int|string $name The child name + */ + public function offsetUnset(mixed $name): void + { + unset($this->children[$name]); + } + + /** + * Returns an iterator to iterate over children (implements \IteratorAggregate). + * + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->children); + } + + public function count(): int + { + return \count($this->children); + } +} diff --git a/lib/symfony/form/Forms.php b/lib/symfony/form/Forms.php new file mode 100644 index 0000000000..020e75eff7 --- /dev/null +++ b/lib/symfony/form/Forms.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * Entry point of the Form component. + * + * Use this class to conveniently create new form factories: + * + * use Symfony\Component\Form\Forms; + * + * $formFactory = Forms::createFormFactory(); + * + * $form = $formFactory->createBuilder() + * ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType') + * ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType') + * ->add('age', 'Symfony\Component\Form\Extension\Core\Type\IntegerType') + * ->add('color', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [ + * 'choices' => ['Red' => 'r', 'Blue' => 'b'], + * ]) + * ->getForm(); + * + * You can also add custom extensions to the form factory: + * + * $formFactory = Forms::createFormFactoryBuilder() + * ->addExtension(new AcmeExtension()) + * ->getFormFactory(); + * + * If you create custom form types or type extensions, it is + * generally recommended to create your own extensions that lazily + * load these types and type extensions. In projects where performance + * does not matter that much, you can also pass them directly to the + * form factory: + * + * $formFactory = Forms::createFormFactoryBuilder() + * ->addType(new PersonType()) + * ->addType(new PhoneNumberType()) + * ->addTypeExtension(new FormTypeHelpTextExtension()) + * ->getFormFactory(); + * + * Support for the Validator component is provided by ValidatorExtension. + * This extension needs a validator object to function properly: + * + * use Symfony\Component\Validator\Validation; + * use Symfony\Component\Form\Extension\Validator\ValidatorExtension; + * + * $validator = Validation::createValidator(); + * $formFactory = Forms::createFormFactoryBuilder() + * ->addExtension(new ValidatorExtension($validator)) + * ->getFormFactory(); + * + * @author Bernhard Schussek + */ +final class Forms +{ + /** + * Creates a form factory with the default configuration. + */ + public static function createFormFactory(): FormFactoryInterface + { + return self::createFormFactoryBuilder()->getFormFactory(); + } + + /** + * Creates a form factory builder with the default configuration. + */ + public static function createFormFactoryBuilder(): FormFactoryBuilderInterface + { + return new FormFactoryBuilder(true); + } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } +} diff --git a/lib/symfony/form/Guess/Guess.php b/lib/symfony/form/Guess/Guess.php new file mode 100644 index 0000000000..d8394f3634 --- /dev/null +++ b/lib/symfony/form/Guess/Guess.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Guess; + +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * Base class for guesses made by FormTypeGuesserInterface implementation. + * + * Each instance contains a confidence value about the correctness of the guess. + * Thus an instance with confidence HIGH_CONFIDENCE is more likely to be + * correct than an instance with confidence LOW_CONFIDENCE. + * + * @author Bernhard Schussek + */ +abstract class Guess +{ + /** + * Marks an instance with a value that is extremely likely to be correct. + */ + public const VERY_HIGH_CONFIDENCE = 3; + + /** + * Marks an instance with a value that is very likely to be correct. + */ + public const HIGH_CONFIDENCE = 2; + + /** + * Marks an instance with a value that is likely to be correct. + */ + public const MEDIUM_CONFIDENCE = 1; + + /** + * Marks an instance with a value that may be correct. + */ + public const LOW_CONFIDENCE = 0; + + /** + * The confidence about the correctness of the value. + * + * One of VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, MEDIUM_CONFIDENCE + * and LOW_CONFIDENCE. + */ + private int $confidence; + + /** + * Returns the guess most likely to be correct from a list of guesses. + * + * If there are multiple guesses with the same, highest confidence, the + * returned guess is any of them. + * + * @param static[] $guesses An array of guesses + */ + public static function getBestGuess(array $guesses): ?static + { + $result = null; + $maxConfidence = -1; + + foreach ($guesses as $guess) { + if ($maxConfidence < $confidence = $guess->getConfidence()) { + $maxConfidence = $confidence; + $result = $guess; + } + } + + return $result; + } + + /** + * @throws InvalidArgumentException if the given value of confidence is unknown + */ + public function __construct(int $confidence) + { + if (self::VERY_HIGH_CONFIDENCE !== $confidence && self::HIGH_CONFIDENCE !== $confidence + && self::MEDIUM_CONFIDENCE !== $confidence && self::LOW_CONFIDENCE !== $confidence) { + throw new InvalidArgumentException('The confidence should be one of the constants defined in Guess.'); + } + + $this->confidence = $confidence; + } + + /** + * Returns the confidence that the guessed value is correct. + * + * @return int One of the constants VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, + * MEDIUM_CONFIDENCE and LOW_CONFIDENCE + */ + public function getConfidence(): int + { + return $this->confidence; + } +} diff --git a/lib/symfony/form/Guess/TypeGuess.php b/lib/symfony/form/Guess/TypeGuess.php new file mode 100644 index 0000000000..8ede78eb8c --- /dev/null +++ b/lib/symfony/form/Guess/TypeGuess.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Guess; + +/** + * Contains a guessed class name and a list of options for creating an instance + * of that class. + * + * @author Bernhard Schussek + */ +class TypeGuess extends Guess +{ + private string $type; + private array $options; + + /** + * @param string $type The guessed field type + * @param array $options The options for creating instances of the + * guessed class + * @param int $confidence The confidence that the guessed class name + * is correct + */ + public function __construct(string $type, array $options, int $confidence) + { + parent::__construct($confidence); + + $this->type = $type; + $this->options = $options; + } + + /** + * Returns the guessed field type. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Returns the guessed options for creating instances of the guessed type. + */ + public function getOptions(): array + { + return $this->options; + } +} diff --git a/lib/symfony/form/Guess/ValueGuess.php b/lib/symfony/form/Guess/ValueGuess.php new file mode 100644 index 0000000000..36abe6602d --- /dev/null +++ b/lib/symfony/form/Guess/ValueGuess.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Guess; + +/** + * Contains a guessed value. + * + * @author Bernhard Schussek + */ +class ValueGuess extends Guess +{ + private string|int|bool|null $value; + + /** + * @param int $confidence The confidence that the guessed class name is correct + */ + public function __construct(string|int|bool|null $value, int $confidence) + { + parent::__construct($confidence); + + $this->value = $value; + } + + /** + * Returns the guessed value. + */ + public function getValue(): string|int|bool|null + { + return $this->value; + } +} diff --git a/lib/symfony/form/LICENSE b/lib/symfony/form/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/form/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/form/NativeRequestHandler.php b/lib/symfony/form/NativeRequestHandler.php new file mode 100644 index 0000000000..a7c775debd --- /dev/null +++ b/lib/symfony/form/NativeRequestHandler.php @@ -0,0 +1,241 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\Form\Util\ServerParams; + +/** + * A request handler using PHP super globals $_GET, $_POST and $_SERVER. + * + * @author Bernhard Schussek + */ +class NativeRequestHandler implements RequestHandlerInterface +{ + private ServerParams $serverParams; + + /** + * The allowed keys of the $_FILES array. + */ + private const FILE_KEYS = [ + 'error', + 'name', + 'size', + 'tmp_name', + 'type', + ]; + + public function __construct(?ServerParams $params = null) + { + $this->serverParams = $params ?? new ServerParams(); + } + + /** + * @return void + * + * @throws UnexpectedTypeException If the $request is not null + */ + public function handleRequest(FormInterface $form, mixed $request = null) + { + if (null !== $request) { + throw new UnexpectedTypeException($request, 'null'); + } + + $name = $form->getName(); + $method = $form->getConfig()->getMethod(); + + if ($method !== self::getRequestMethod()) { + return; + } + + // For request methods that must not have a request body we fetch data + // from the query string. Otherwise we look for data in the request body. + if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) { + if ('' === $name) { + $data = $_GET; + } else { + // Don't submit GET requests if the form's name does not exist + // in the request + if (!isset($_GET[$name])) { + return; + } + + $data = $_GET[$name]; + } + } else { + // Mark the form with an error if the uploaded size was too large + // This is done here and not in FormValidator because $_POST is + // empty when that error occurs. Hence the form is never submitted. + if ($this->serverParams->hasPostMaxSizeBeenExceeded()) { + // Submit the form, but don't clear the default values + $form->submit(null, false); + + $form->addError(new FormError( + $form->getConfig()->getOption('upload_max_size_message')(), + null, + ['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()] + )); + + return; + } + + $fixedFiles = []; + foreach ($_FILES as $fileKey => $file) { + $fixedFiles[$fileKey] = self::stripEmptyFiles(self::fixPhpFilesArray($file)); + } + + if ('' === $name) { + $params = $_POST; + $files = $fixedFiles; + } elseif (\array_key_exists($name, $_POST) || \array_key_exists($name, $fixedFiles)) { + $default = $form->getConfig()->getCompound() ? [] : null; + $params = \array_key_exists($name, $_POST) ? $_POST[$name] : $default; + $files = \array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default; + } else { + // Don't submit the form if it is not present in the request + return; + } + + if (\is_array($params) && \is_array($files)) { + $data = FormUtil::mergeParamsAndFiles($params, $files); + } else { + $data = $params ?: $files; + } + } + + // Don't auto-submit the form unless at least one field is present. + if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) { + return; + } + + if (\is_array($data) && \array_key_exists('_method', $data) && $method === $data['_method'] && !$form->has('_method')) { + unset($data['_method']); + } + + $form->submit($data, 'PATCH' !== $method); + } + + public function isFileUpload(mixed $data): bool + { + // POST data will always be strings or arrays of strings. Thus, we can be sure + // that the submitted data is a file upload if the "error" value is an integer + // (this value must have been injected by PHP itself). + return \is_array($data) && isset($data['error']) && \is_int($data['error']); + } + + public function getUploadFileError(mixed $data): ?int + { + if (!\is_array($data)) { + return null; + } + + if (!isset($data['error'])) { + return null; + } + + if (!\is_int($data['error'])) { + return null; + } + + if (\UPLOAD_ERR_OK === $data['error']) { + return null; + } + + return $data['error']; + } + + private static function getRequestMethod(): string + { + $method = isset($_SERVER['REQUEST_METHOD']) + ? strtoupper($_SERVER['REQUEST_METHOD']) + : 'GET'; + + if ('POST' === $method && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } + + return $method; + } + + /** + * Fixes a malformed PHP $_FILES array. + * + * PHP has a bug that the format of the $_FILES array differs, depending on + * whether the uploaded file fields had normal field names or array-like + * field names ("normal" vs. "parent[child]"). + * + * This method fixes the array to look like the "normal" $_FILES array. + * + * It's safe to pass an already converted array, in which case this method + * just returns the original array unmodified. + * + * This method is identical to {@link \Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray} + * and should be kept as such in order to port fixes quickly and easily. + */ + private static function fixPhpFilesArray(mixed $data): mixed + { + if (!\is_array($data)) { + return $data; + } + + // Remove extra key added by PHP 8.1. + unset($data['full_path']); + $keys = array_keys($data); + sort($keys); + + if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { + return $data; + } + + $files = $data; + foreach (self::FILE_KEYS as $k) { + unset($files[$k]); + } + + foreach ($data['name'] as $key => $name) { + $files[$key] = self::fixPhpFilesArray([ + 'error' => $data['error'][$key], + 'name' => $name, + 'type' => $data['type'][$key], + 'tmp_name' => $data['tmp_name'][$key], + 'size' => $data['size'][$key], + ]); + } + + return $files; + } + + private static function stripEmptyFiles(mixed $data): mixed + { + if (!\is_array($data)) { + return $data; + } + + $keys = array_keys($data); + sort($keys); + + if (self::FILE_KEYS === $keys) { + if (\UPLOAD_ERR_NO_FILE === $data['error']) { + return null; + } + + return $data; + } + + foreach ($data as $key => $value) { + $data[$key] = self::stripEmptyFiles($value); + } + + return $data; + } +} diff --git a/lib/symfony/form/PreloadedExtension.php b/lib/symfony/form/PreloadedExtension.php new file mode 100644 index 0000000000..90eefb1f1b --- /dev/null +++ b/lib/symfony/form/PreloadedExtension.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * A form extension with preloaded types, type extensions and type guessers. + * + * @author Bernhard Schussek + */ +class PreloadedExtension implements FormExtensionInterface +{ + private array $types = []; + private array $typeExtensions = []; + private ?FormTypeGuesserInterface $typeGuesser; + + /** + * Creates a new preloaded extension. + * + * @param FormTypeInterface[] $types The types that the extension should support + * @param FormTypeExtensionInterface[][] $typeExtensions The type extensions that the extension should support + */ + public function __construct(array $types, array $typeExtensions, ?FormTypeGuesserInterface $typeGuesser = null) + { + $this->typeExtensions = $typeExtensions; + $this->typeGuesser = $typeGuesser; + + foreach ($types as $type) { + $this->types[$type::class] = $type; + } + } + + public function getType(string $name): FormTypeInterface + { + if (!isset($this->types[$name])) { + throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name)); + } + + return $this->types[$name]; + } + + public function hasType(string $name): bool + { + return isset($this->types[$name]); + } + + public function getTypeExtensions(string $name): array + { + return $this->typeExtensions[$name] + ?? []; + } + + public function hasTypeExtensions(string $name): bool + { + return !empty($this->typeExtensions[$name]); + } + + public function getTypeGuesser(): ?FormTypeGuesserInterface + { + return $this->typeGuesser; + } +} diff --git a/lib/symfony/form/README.md b/lib/symfony/form/README.md new file mode 100644 index 0000000000..0cda654d73 --- /dev/null +++ b/lib/symfony/form/README.md @@ -0,0 +1,13 @@ +Form Component +============== + +The Form component allows you to easily create, process and reuse HTML forms. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/form.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/lib/symfony/form/RequestHandlerInterface.php b/lib/symfony/form/RequestHandlerInterface.php new file mode 100644 index 0000000000..39fd458ee4 --- /dev/null +++ b/lib/symfony/form/RequestHandlerInterface.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\Form; + +/** + * Submits forms if they were submitted. + * + * @author Bernhard Schussek + */ +interface RequestHandlerInterface +{ + /** + * Submits a form if it was submitted. + * + * @return void + */ + public function handleRequest(FormInterface $form, mixed $request = null); + + /** + * Returns true if the given data is a file upload. + */ + public function isFileUpload(mixed $data): bool; +} diff --git a/lib/symfony/form/ResolvedFormType.php b/lib/symfony/form/ResolvedFormType.php new file mode 100644 index 0000000000..2b2f747c7f --- /dev/null +++ b/lib/symfony/form/ResolvedFormType.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * A wrapper for a form type and its extensions. + * + * @author Bernhard Schussek + */ +class ResolvedFormType implements ResolvedFormTypeInterface +{ + private FormTypeInterface $innerType; + + /** + * @var FormTypeExtensionInterface[] + */ + private array $typeExtensions; + + private ?ResolvedFormTypeInterface $parent; + + private OptionsResolver $optionsResolver; + + /** + * @param FormTypeExtensionInterface[] $typeExtensions + */ + public function __construct(FormTypeInterface $innerType, array $typeExtensions = [], ?ResolvedFormTypeInterface $parent = null) + { + foreach ($typeExtensions as $extension) { + if (!$extension instanceof FormTypeExtensionInterface) { + throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class); + } + } + + $this->innerType = $innerType; + $this->typeExtensions = $typeExtensions; + $this->parent = $parent; + } + + public function getBlockPrefix(): string + { + return $this->innerType->getBlockPrefix(); + } + + public function getParent(): ?ResolvedFormTypeInterface + { + return $this->parent; + } + + public function getInnerType(): FormTypeInterface + { + return $this->innerType; + } + + public function getTypeExtensions(): array + { + return $this->typeExtensions; + } + + public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface + { + try { + $options = $this->getOptionsResolver()->resolve($options); + } catch (ExceptionInterface $e) { + throw new $e(\sprintf('An error has occurred resolving the options of the form "%s": ', get_debug_type($this->getInnerType())).$e->getMessage(), $e->getCode(), $e); + } + + // Should be decoupled from the specific option at some point + $dataClass = $options['data_class'] ?? null; + + $builder = $this->newBuilder($name, $dataClass, $factory, $options); + $builder->setType($this); + + return $builder; + } + + public function createView(FormInterface $form, ?FormView $parent = null): FormView + { + return $this->newView($parent); + } + + /** + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $this->parent?->buildForm($builder, $options); + + $this->innerType->buildForm($builder, $options); + + foreach ($this->typeExtensions as $extension) { + $extension->buildForm($builder, $options); + } + } + + /** + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $this->parent?->buildView($view, $form, $options); + + $this->innerType->buildView($view, $form, $options); + + foreach ($this->typeExtensions as $extension) { + $extension->buildView($view, $form, $options); + } + } + + /** + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->parent?->finishView($view, $form, $options); + + $this->innerType->finishView($view, $form, $options); + + /** @var FormTypeExtensionInterface $extension */ + foreach ($this->typeExtensions as $extension) { + $extension->finishView($view, $form, $options); + } + } + + public function getOptionsResolver(): OptionsResolver + { + if (!isset($this->optionsResolver)) { + if (null !== $this->parent) { + $this->optionsResolver = clone $this->parent->getOptionsResolver(); + } else { + $this->optionsResolver = new OptionsResolver(); + } + + $this->innerType->configureOptions($this->optionsResolver); + + foreach ($this->typeExtensions as $extension) { + $extension->configureOptions($this->optionsResolver); + } + } + + return $this->optionsResolver; + } + + /** + * Creates a new builder instance. + * + * Override this method if you want to customize the builder class. + */ + protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface + { + if ($this->innerType instanceof ButtonTypeInterface) { + return new ButtonBuilder($name, $options); + } + + if ($this->innerType instanceof SubmitButtonTypeInterface) { + return new SubmitButtonBuilder($name, $options); + } + + return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options); + } + + /** + * Creates a new view instance. + * + * Override this method if you want to customize the view class. + */ + protected function newView(?FormView $parent = null): FormView + { + return new FormView($parent); + } +} diff --git a/lib/symfony/form/ResolvedFormTypeFactory.php b/lib/symfony/form/ResolvedFormTypeFactory.php new file mode 100644 index 0000000000..437f9c553c --- /dev/null +++ b/lib/symfony/form/ResolvedFormTypeFactory.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\Form; + +/** + * @author Bernhard Schussek + */ +class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface +{ + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface + { + return new ResolvedFormType($type, $typeExtensions, $parent); + } +} diff --git a/lib/symfony/form/ResolvedFormTypeFactoryInterface.php b/lib/symfony/form/ResolvedFormTypeFactoryInterface.php new file mode 100644 index 0000000000..9fd39e7fe2 --- /dev/null +++ b/lib/symfony/form/ResolvedFormTypeFactoryInterface.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\Form; + +/** + * Creates ResolvedFormTypeInterface instances. + * + * This interface allows you to use your custom ResolvedFormTypeInterface + * implementation, within which you can customize the concrete FormBuilderInterface + * implementations or FormView subclasses that are used by the framework. + * + * @author Bernhard Schussek + */ +interface ResolvedFormTypeFactoryInterface +{ + /** + * Resolves a form type. + * + * @param FormTypeExtensionInterface[] $typeExtensions + * + * @throws Exception\UnexpectedTypeException if the types parent {@link FormTypeInterface::getParent()} is not a string + * @throws Exception\InvalidArgumentException if the types parent cannot be retrieved from any extension + */ + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface; +} diff --git a/lib/symfony/form/ResolvedFormTypeInterface.php b/lib/symfony/form/ResolvedFormTypeInterface.php new file mode 100644 index 0000000000..e6f67ed403 --- /dev/null +++ b/lib/symfony/form/ResolvedFormTypeInterface.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * A wrapper for a form type and its extensions. + * + * @author Bernhard Schussek + */ +interface ResolvedFormTypeInterface +{ + /** + * Returns the prefix of the template block name for this type. + */ + public function getBlockPrefix(): string; + + /** + * Returns the parent type. + */ + public function getParent(): ?self; + + /** + * Returns the wrapped form type. + */ + public function getInnerType(): FormTypeInterface; + + /** + * Returns the extensions of the wrapped form type. + * + * @return FormTypeExtensionInterface[] + */ + public function getTypeExtensions(): array; + + /** + * Creates a new form builder for this type. + * + * @param string $name The name for the builder + */ + public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface; + + /** + * Creates a new form view for a form of this type. + */ + public function createView(FormInterface $form, ?FormView $parent = null): FormView; + + /** + * Configures a form builder for the type hierarchy. + * + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options); + + /** + * Configures a form view for the type hierarchy. + * + * It is called before the children of the view are built. + * + * @return void + */ + public function buildView(FormView $view, FormInterface $form, array $options); + + /** + * Finishes a form view for the type hierarchy. + * + * It is called after the children of the view have been built. + * + * @return void + */ + public function finishView(FormView $view, FormInterface $form, array $options); + + /** + * Returns the configured options resolver used for this type. + */ + public function getOptionsResolver(): OptionsResolver; +} diff --git a/lib/symfony/form/Resources/config/validation.xml b/lib/symfony/form/Resources/config/validation.xml new file mode 100644 index 0000000000..918f101f42 --- /dev/null +++ b/lib/symfony/form/Resources/config/validation.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/lib/symfony/form/Resources/translations/validators.af.xlf b/lib/symfony/form/Resources/translations/validators.af.xlf new file mode 100644 index 0000000000..c726e93b9e --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.af.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Hierdie vorm moet nie ekstra velde bevat nie. + + + The uploaded file was too large. Please try to upload a smaller file. + Die opgelaaide lêer was te groot. Probeer asseblief 'n kleiner lêer. + + + The CSRF token is invalid. Please try to resubmit the form. + Die CSRF-teken is ongeldig. Probeer asseblief om die vorm weer in te dien. + + + This value is not a valid HTML5 color. + Hierdie waarde is nie 'n geldige HTML5 kleur nie. + + + Please enter a valid birthdate. + Voer asseblief 'n geldige geboortedatum in. + + + The selected choice is invalid. + Die gekiesde opsie is nie geldig nie. + + + The collection is invalid. + Die versameling is nie geldig nie. + + + Please select a valid color. + Kies asseblief 'n geldige kleur. + + + Please select a valid country. + Kies asseblief 'n geldige land. + + + Please select a valid currency. + Kies asseblief 'n geldige geldeenheid. + + + Please choose a valid date interval. + Kies asseblief 'n geldige datum interval. + + + Please enter a valid date and time. + Voer asseblilef 'n geldige datum en tyd in. + + + Please enter a valid date. + Voer asseblief 'n geldige datum in. + + + Please select a valid file. + Kies asseblief 'n geldige lêer. + + + The hidden field is invalid. + Die versteekte veld is nie geldig nie. + + + Please enter an integer. + Voer asseblief 'n geldige heeltal in. + + + Please select a valid language. + Kies assblief 'n geldige taal. + + + Please select a valid locale. + Voer assebliefn 'n geldige locale in. + + + Please enter a valid money amount. + Voer asseblief 'n geldige bedrag in. + + + Please enter a number. + Voer asseblief 'n nommer in. + + + The password is invalid. + Die wagwoord is ongeldig. + + + Please enter a percentage value. + Voer asseblief 'n geldige persentasie waarde in. + + + The values do not match. + Die waardes is nie dieselfde nie. + + + Please enter a valid time. + Voer asseblief 'n geldige tyd in time. + + + Please select a valid timezone. + Kies asseblief 'n geldige tydsone. + + + Please enter a valid URL. + Voer asseblief 'n geldige URL in. + + + Please enter a valid search term. + Voer asseblief 'n geldige soek term in. + + + Please provide a valid phone number. + Verskaf asseblief 'n geldige telefoonnommer. + + + The checkbox has an invalid value. + Die blokkie het 'n ongeldige waarde. + + + Please enter a valid email address. + Voer asseblief 'n geldige e-pos adres in. + + + Please select a valid option. + Kies asseblief 'n geldige opsie. + + + Please select a valid range. + Kies asseblief 'n geldige reeks. + + + Please enter a valid week. + Voer assblief 'n geldige week in. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.ar.xlf b/lib/symfony/form/Resources/translations/validators.ar.xlf new file mode 100644 index 0000000000..d18b4691e1 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.ar.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + هذا النموذج يجب الا يحتوى على اى حقول اضافية. + + + The uploaded file was too large. Please try to upload a smaller file. + مساحة الملف المرسل كبيرة. من فضلك حاول ارسال ملف اصغر. + + + The CSRF token is invalid. Please try to resubmit the form. + قيمة رمز الموقع غير صحيحة. من فضلك اعد ارسال النموذج. + + + This value is not a valid HTML5 color. + هذه القيمة ليست لون HTML5 صالحًا. + + + Please enter a valid birthdate. + الرجاء ادخال تاريخ ميلاد صالح. + + + The selected choice is invalid. + الاختيار المحدد غير صالح. + + + The collection is invalid. + المجموعة غير صالحة. + + + Please select a valid color. + الرجاء اختيار لون صالح. + + + Please select a valid country. + الرجاء اختيار بلد صالح. + + + Please select a valid currency. + الرجاء اختيار عملة صالحة. + + + Please choose a valid date interval. + الرجاء اختيار فاصل زمني صالح. + + + Please enter a valid date and time. + الرجاء إدخال تاريخ ووقت صالحين. + + + Please enter a valid date. + الرجاء إدخال تاريخ صالح. + + + Please select a valid file. + الرجاء اختيار ملف صالح. + + + The hidden field is invalid. + الحقل المخفي غير صالح. + + + Please enter an integer. + الرجاء إدخال عدد صحيح. + + + Please select a valid language. + الرجاء اختيار لغة صالحة. + + + Please select a valid locale. + الرجاء اختيار لغة صالحة. + + + Please enter a valid money amount. + الرجاء إدخال مبلغ مالي صالح. + + + Please enter a number. + الرجاء إدخال رقم. + + + The password is invalid. + كلمة المرور غير صحيحة. + + + Please enter a percentage value. + الرجاء إدخال قيمة النسبة المئوية. + + + The values do not match. + القيم لا تتطابق. + + + Please enter a valid time. + الرجاء إدخال وقت صالح. + + + Please select a valid timezone. + الرجاء تحديد منطقة زمنية صالحة. + + + Please enter a valid URL. + أدخل عنوان الرابط صحيح من فضلك. + + + Please enter a valid search term. + الرجاء إدخال مصطلح البحث ساري المفعول. + + + Please provide a valid phone number. + يرجى تقديم رقم هاتف صالح. + + + The checkbox has an invalid value. + خانة الاختيار لها قيمة غير صالحة. + + + Please enter a valid email address. + رجاء قم بإدخال بريد الكتروني صحيح + + + Please select a valid option. + الرجاء تحديد خيار صالح. + + + Please select a valid range. + يرجى تحديد نطاق صالح. + + + Please enter a valid week. + الرجاء إدخال أسبوع صالح. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.az.xlf b/lib/symfony/form/Resources/translations/validators.az.xlf new file mode 100644 index 0000000000..87791b6d42 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.az.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Bu formada əlavə sahə olmamalıdır. + + + The uploaded file was too large. Please try to upload a smaller file. + Yüklənən fayl çox böyükdür. Lütfən daha kiçik fayl yükləyin. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF nişanı yanlışdır. Lütfen formanı yenidən göndərin. + + + This value is not a valid HTML5 color. + Bu dəyər doğru bir HTML5 rəngi deyil. + + + Please enter a valid birthdate. + Zəhmət olmasa doğru bir doğum günü daxil edin. + + + The selected choice is invalid. + Seçilmiş seçim doğru deyil. + + + The collection is invalid. + Kolleksiya doğru deyil. + + + Please select a valid color. + Zəhmət olmasa doğru bir rəng seçin. + + + Please select a valid country. + Zəhmət olmasa doğru bir ölkə seçin. + + + Please select a valid currency. + Zəhmət olmasa doğru bir valyuta seçin. + + + Please choose a valid date interval. + Zəhmət olmasa doğru bir tarix aralığı seçin. + + + Please enter a valid date and time. + Zəhmət olmasa doğru bir tarix ve saat daxil edin. + + + Please enter a valid date. + Zəhmət olmasa doğru bir tarix daxil edin. + + + Please select a valid file. + Zəhmət olmasa doğru bir fayl seçin. + + + The hidden field is invalid. + Gizli sahə doğru deyil. + + + Please enter an integer. + Zəhmət olmasa bir tam ədəd daxil edin. + + + Please select a valid language. + Zəhmət olmasa doğru bir dil seçin. + + + Please select a valid locale. + Zəhmət olmasa doğru bir yer seçin. + + + Please enter a valid money amount. + Zəhmət olmasa doğru bir pul miqdarı daxil edin. + + + Please enter a number. + Zəhmət olmasa doğru bir rəqəm daxil edin. + + + The password is invalid. + Parol doğru deyil. + + + Please enter a percentage value. + Zəhmət olmasa doğru bir faiz dəyəri daxil edin. + + + The values do not match. + Dəyərlər örtüşmür. + + + Please enter a valid time. + Zəhmət olmasa doğru bir saat daxil edin. + + + Please select a valid timezone. + Zəhmət olmasa doğru bir saat qurşağı seçin. + + + Please enter a valid URL. + Zəhmət olmasa doğru bir URL daxil edin. + + + Please enter a valid search term. + Zəhmət olmasa doğru bir axtarış termini daxil edin. + + + Please provide a valid phone number. + Zəhmət olmasa doğru bir telefon nömrəsi seçin. + + + The checkbox has an invalid value. + Seçim qutusunda doğru olmayan dəyər var. + + + Please enter a valid email address. + Zəhmət olmasa doğru bir e-poçt seçin. + + + Please select a valid option. + Zəhmət olmasa doğru bir variant seçin. + + + Please select a valid range. + Zəhmət olmasa doğru bir aralıq seçin. + + + Please enter a valid week. + Zəhmət olmasa doğru bir həftə seçin. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.be.xlf b/lib/symfony/form/Resources/translations/validators.be.xlf new file mode 100644 index 0000000000..b24976e13c --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.be.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Гэта форма не павінна мець дадатковых палей. + + + The uploaded file was too large. Please try to upload a smaller file. + Запампаваны файл быў занадта вялікім. Калі ласка, паспрабуйце запампаваць файл меншага памеру. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-токен не сапраўдны. Калі ласка, паспрабуйце яшчэ раз адправіць форму. + + + This value is not a valid HTML5 color. + Значэнне не з'яўляецца карэктным HTML5 колерам. + + + Please enter a valid birthdate. + Калі ласка, увядзіце карэктную дату нараджэння. + + + The selected choice is invalid. + Выбраны варыянт некарэктны. + + + The collection is invalid. + Калекцыя некарэктна. + + + Please select a valid color. + Калі ласка, выберыце карэктны колер. + + + Please select a valid country. + Калі ласка, выберыце карэктную краіну. + + + Please select a valid currency. + Калі ласка, выберыце карэктную валюту. + + + Please choose a valid date interval. + Калі ласка, выберыце карэктны інтэрвал дат. + + + Please enter a valid date and time. + Калі ласка, увядзіце карэктныя дату і час. + + + Please enter a valid date. + Калі ласка, увядзіце карэктную дату. + + + Please select a valid file. + Калі ласка, выберыце карэктны файл. + + + The hidden field is invalid. + Значэнне схаванага поля некарэктна. + + + Please enter an integer. + Калі ласка, увядзіце цэлы лік. + + + Please select a valid language. + Калі ласка, выберыце карэктную мову. + + + Please select a valid locale. + Калі ласка, выберыце карэктную лакаль. + + + Please enter a valid money amount. + Калі ласка, увядзіце карэктную колькасць грошай. + + + Please enter a number. + Калі ласка, увядзіце нумар. + + + The password is invalid. + Няправільны пароль. + + + Please enter a percentage value. + Калі ласка, увядзіце працэнтнае значэнне. + + + The values do not match. + Значэнні не супадаюць. + + + Please enter a valid time. + Калі ласка, увядзіце карэктны час. + + + Please select a valid timezone. + Калі ласка, выберыце карэктны гадзінны пояс. + + + Please enter a valid URL. + Калі ласка, увядзіце карэктны URL. + + + Please enter a valid search term. + Калі ласка, увядзіце карэктны пошукавы запыт. + + + Please provide a valid phone number. + Калі ласка, увядзіце карэктны нумар тэлефона. + + + The checkbox has an invalid value. + Флажок мае некарэктнае значэнне. + + + Please enter a valid email address. + Калі ласка, увядзіце карэктны адрас электроннай пошты. + + + Please select a valid option. + Калі ласка, выберыце карэктны варыянт. + + + Please select a valid range. + Калі ласка, выберыце карэктны дыяпазон. + + + Please enter a valid week. + Калі ласка, увядзіце карэктны тыдзень. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.bg.xlf b/lib/symfony/form/Resources/translations/validators.bg.xlf new file mode 100644 index 0000000000..19b80f5f8f --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.bg.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Тази форма не трябва да съдържа допълнителни полета. + + + The uploaded file was too large. Please try to upload a smaller file. + Каченият файл е твърде голям. Моля, опитайте да качите по-малък файл. + + + The CSRF token is invalid. Please try to resubmit the form. + Невалиден CSRF токен. Моля, опитайте да изпратите формата отново. + + + This value is not a valid HTML5 color. + Стойността не е валиден HTML5 цвят. + + + Please enter a valid birthdate. + Моля въведете валидна дата на раждане. + + + The selected choice is invalid. + Избраните стойности не са валидни. + + + The collection is invalid. + Колекцията не е валидна. + + + Please select a valid color. + Моля изберете валиден цвят. + + + Please select a valid country. + Моля изберете валидна държава. + + + Please select a valid currency. + Моля изберете валидна валута. + + + Please choose a valid date interval. + Моля изберете валиден интервал от дати. + + + Please enter a valid date and time. + Моля въведете валидни дата и час. + + + Please enter a valid date. + Моля въведете валидна дата. + + + Please select a valid file. + Моля изберете валиден файл. + + + The hidden field is invalid. + Скритото поле е невалидно. + + + Please enter an integer. + Моля попълнете цяло число. + + + Please select a valid language. + Моля изберете валиден език. + + + Please select a valid locale. + Моля изберете валиден език. + + + Please enter a valid money amount. + Моля въведете валидна парична сума. + + + Please enter a number. + Моля въведете число. + + + The password is invalid. + Паролата е невалидна. + + + Please enter a percentage value. + Моля въведете процентна стойност. + + + The values do not match. + Стойностите не съвпадат. + + + Please enter a valid time. + Моля въведете валидно време. + + + Please select a valid timezone. + Моля изберете валидна часова зона. + + + Please enter a valid URL. + Моля въведете валиден URL. + + + Please enter a valid search term. + Моля въведете валидно търсене. + + + Please provide a valid phone number. + Моля осигурете валиден телефонен номер. + + + The checkbox has an invalid value. + Отметката има невалидна стойност. + + + Please enter a valid email address. + Моля въведете валидна ел. поща. + + + Please select a valid option. + Моля изберете валидна опция. + + + Please select a valid range. + Моля изберете валиден обхват. + + + Please enter a valid week. + Моля въведете валидна седмица. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.bs.xlf b/lib/symfony/form/Resources/translations/validators.bs.xlf new file mode 100644 index 0000000000..d360635dfc --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.bs.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ovaj obrazac ne bi trebalo da sadrži dodatna polja. + + + The uploaded file was too large. Please try to upload a smaller file. + Prenijeta (uploaded) datoteka je prevelika. Molim pokušajte prenijeti manju datoteku. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF vrijednost nije ispravna. Molim pokušajte ponovo da pošaljete obrazac. + + + This value is not a valid HTML5 color. + Ova vrijednost nije važeća HTML5 boja. + + + Please enter a valid birthdate. + Molim upišite ispravan datum rođenja. + + + The selected choice is invalid. + Odabrani izbor nije ispravan. + + + The collection is invalid. + Ova kolekcija nije ispravna. + + + Please select a valid color. + Molim izaberite ispravnu boju. + + + Please select a valid country. + Molim izaberite ispravnu državu. + + + Please select a valid currency. + Molim izaberite ispravnu valutu. + + + Please choose a valid date interval. + Molim izaberite ispravan datumski interval. + + + Please enter a valid date and time. + Molim upišite ispravan datum i vrijeme. + + + Please enter a valid date. + Molim upišite ispravan datum. + + + Please select a valid file. + Molim izaberite ispravnu datoteku. + + + The hidden field is invalid. + Skriveno polje nije ispravno. + + + Please enter an integer. + Molim upišite cijeli broj (integer). + + + Please select a valid language. + Molim izaberite ispravan jezik. + + + Please select a valid locale. + Molim izaberite ispravnu lokalizaciju. + + + Please enter a valid money amount. + Molim upišite ispravnu količinu novca. + + + Please enter a number. + Molim upišite broj. + + + The password is invalid. + Ova lozinka nije ispravna. + + + Please enter a percentage value. + Molim upišite procentualnu vrijednost. + + + The values do not match. + Date vrijednosti se ne poklapaju. + + + Please enter a valid time. + Molim upišite ispravno vrijeme. + + + Please select a valid timezone. + Molim izaberite ispravnu vremensku zonu. + + + Please enter a valid URL. + Molim upišite ispravan URL. + + + Please enter a valid search term. + Molim upišite ispravan termin za pretragu. + + + Please provide a valid phone number. + Molim navedite ispravan broj telefona. + + + The checkbox has an invalid value. + Polje za potvrdu sadrži neispravnu vrijednost. + + + Please enter a valid email address. + Molim upišite ispravnu email adresu. + + + Please select a valid option. + Molim izaberite ispravnu opciju. + + + Please select a valid range. + Molim izaberite ispravan opseg. + + + Please enter a valid week. + Molim upišite ispravnu sedmicu. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.ca.xlf b/lib/symfony/form/Resources/translations/validators.ca.xlf new file mode 100644 index 0000000000..dbf9ea8320 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.ca.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Aquest formulari no hauria de contenir camps addicionals. + + + The uploaded file was too large. Please try to upload a smaller file. + El fitxer pujat és massa gran. Pujeu un fitxer més petit. + + + The CSRF token is invalid. Please try to resubmit the form. + El token CSRF no és vàlid. Torneu a enviar el formulari. + + + This value is not a valid HTML5 color. + Aquest valor no és un color HTML5 vàlid. + + + Please enter a valid birthdate. + Introduïu una data d'aniversari vàlida. + + + The selected choice is invalid. + L'opció escollida no és vàlida. + + + The collection is invalid. + La col·lecció no és vàlida. + + + Please select a valid color. + Seleccioneu un color vàlid. + + + Please select a valid country. + Seleccioneu una ciutat vàlida. + + + Please select a valid currency. + Seleccioneu una moneda vàlida. + + + Please choose a valid date interval. + Escolliu un interval de dates vàlides. + + + Please enter a valid date and time. + Introduïu una data i hora vàlides. + + + Please enter a valid date. + Introduïu una data vàlida. + + + Please select a valid file. + Seleccioneu un fitxer vàlid. + + + The hidden field is invalid. + El camp ocult no és vàlid. + + + Please enter an integer. + Introduïu un enter. + + + Please select a valid language. + Seleccioneu un idioma vàlid. + + + Please select a valid locale. + Seleccioneu una configuració regional vàlida + + + Please enter a valid money amount. + Introduïu una quantitat de diners vàlida. + + + Please enter a number. + Introduïu un número. + + + The password is invalid. + La contrasenya no és vàlida. + + + Please enter a percentage value. + Introduïu un valor percentual. + + + The values do not match. + Els valors no coincideixen. + + + Please enter a valid time. + Introduïu una hora vàlida. + + + Please select a valid timezone. + Seleccioneu una zona horària vàlida. + + + Please enter a valid URL. + Introduïu una URL vàlida. + + + Please enter a valid search term. + Introduïu un terme de cerca vàlid. + + + Please provide a valid phone number. + Introduïu un número de telèfon vàlid. + + + The checkbox has an invalid value. + La casella de selecció te un valor no vàlid. + + + Please enter a valid email address. + Introduïu un correu electrònic vàlid. + + + Please select a valid option. + Seleccioneu una opció vàlida. + + + Please select a valid range. + Seleccioneu un rang vàlid. + + + Please enter a valid week. + Introduïu una setmana vàlida. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.cs.xlf b/lib/symfony/form/Resources/translations/validators.cs.xlf new file mode 100644 index 0000000000..829fea17b1 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.cs.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Tato skupina polí nesmí obsahovat další pole. + + + The uploaded file was too large. Please try to upload a smaller file. + Nahraný soubor je příliš velký. Nahrajte prosím menší soubor. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF token je neplatný. Zkuste prosím znovu odeslat formulář. + + + This value is not a valid HTML5 color. + Tato hodnota není platná HTML5 barva. + + + Please enter a valid birthdate. + Prosím zadejte platný datum narození. + + + The selected choice is invalid. + Vybraná možnost není platná. + + + The collection is invalid. + Kolekce není platná. + + + Please select a valid color. + Prosím vyberte platnou barvu. + + + Please select a valid country. + Prosím vyberte platnou zemi. + + + Please select a valid currency. + Prosím vyberte platnou měnu. + + + Please choose a valid date interval. + Prosím vyberte platné rozpětí dat. + + + Please enter a valid date and time. + Prosím zadejte platný datum a čas. + + + Please enter a valid date. + Prosím zadejte platný datum. + + + Please select a valid file. + Prosím vyberte platný soubor. + + + The hidden field is invalid. + Skryté pole není platné. + + + Please enter an integer. + Prosím zadejte číslo. + + + Please select a valid language. + Prosím zadejte platný jazyk. + + + Please select a valid locale. + Prosím zadejte platný jazyk. + + + Please enter a valid money amount. + Prosím zadejte platnou částku. + + + Please enter a number. + Prosím zadejte číslo. + + + The password is invalid. + Heslo není platné. + + + Please enter a percentage value. + Prosím zadejte procentuální hodnotu. + + + The values do not match. + Hodnoty se neshodují. + + + Please enter a valid time. + Prosím zadejte platný čas. + + + Please select a valid timezone. + Prosím vyberte platné časové pásmo. + + + Please enter a valid URL. + Prosím zadejte platnou URL. + + + Please enter a valid search term. + Prosím zadejte platný výraz k vyhledání. + + + Please provide a valid phone number. + Prosím zadejte platné telefonní číslo. + + + The checkbox has an invalid value. + Zaškrtávací políčko má neplatnou hodnotu. + + + Please enter a valid email address. + Prosím zadejte platnou emailovou adresu. + + + Please select a valid option. + Prosím vyberte platnou možnost. + + + Please select a valid range. + Prosím vyberte platný rozsah. + + + Please enter a valid week. + Prosím zadejte platný týden. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.cy.xlf b/lib/symfony/form/Resources/translations/validators.cy.xlf new file mode 100644 index 0000000000..48f18afe7c --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.cy.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ni ddylai'r ffurflen gynnwys meysydd ychwanegol. + + + The uploaded file was too large. Please try to upload a smaller file. + Roedd y ffeil a uwchlwythwyd yn rhy fawr. Ceisiwch uwchlwytho ffeil llai. + + + The CSRF token is invalid. Please try to resubmit the form. + Mae'r tocyn CSRF yn annilys. Ceisiwch ailgyflwyno'r ffurflen. + + + This value is not a valid HTML5 color. + Nid yw'r gwerth hwn yn lliw HTML5 dilys. + + + Please enter a valid birthdate. + Nodwch ddyddiad geni dilys. + + + The selected choice is invalid. + Mae'r dewis a ddewiswyd yn annilys. + + + The collection is invalid. + Mae'r casgliad yn annilys. + + + Please select a valid color. + Dewiswch liw dilys. + + + Please select a valid country. + Dewiswch wlad ddilys. + + + Please select a valid currency. + Dewiswch arian cyfred dilys. + + + Please choose a valid date interval. + Dewiswch ystod dyddiadau dilys. + + + Please enter a valid date and time. + Nodwch ddyddiad ac amser dilys. + + + Please enter a valid date. + Nodwch ddyddiad dilys. + + + Please select a valid file. + Dewiswch ffeil ddilys. + + + The hidden field is invalid. + Mae'r maes cudd yn annilys. + + + Please enter an integer. + Nodwch rif cyfan. + + + Please select a valid language. + Dewiswch iaith ddilys. + + + Please select a valid locale. + Dewiswch leoliad dilys. + + + Please enter a valid money amount. + Nodwch swm arian dilys. + + + Please enter a number. + Nodwch rif. + + + The password is invalid. + Mae'r cyfrinair yn annilys. + + + Please enter a percentage value. + Nodwch werth canran. + + + The values do not match. + Nid yw'r gwerthoedd yn cyfateb. + + + Please enter a valid time. + Nodwch amser dilys. + + + Please select a valid timezone. + Dewiswch barth amser dilys. + + + Please enter a valid URL. + Nodwch URL dilys. + + + Please enter a valid search term. + Nodwch derm chwilio dilys. + + + Please provide a valid phone number. + Darparwch rif ffôn dilys. + + + The checkbox has an invalid value. + Mae gan y blwch ticio werth annilys. + + + Please enter a valid email address. + Nodwch gyfeiriad e-bost dilys. + + + Please select a valid option. + Dewiswch opsiwn dilys. + + + Please select a valid range. + Dewiswch ystod ddilys. + + + Please enter a valid week. + Nodwch wythnos ddilys. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.da.xlf b/lib/symfony/form/Resources/translations/validators.da.xlf new file mode 100644 index 0000000000..36f49b2c89 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.da.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Feltgruppen må ikke indeholde ekstra felter. + + + The uploaded file was too large. Please try to upload a smaller file. + Den uploadede fil var for stor. Upload venligst en mindre fil. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-token er ugyldig. Prøv venligst at genindsende. + + + This value is not a valid HTML5 color. + Værdien er ikke en gyldig HTML5 farve. + + + Please enter a valid birthdate. + Indtast venligst en gyldig fødselsdato. + + + The selected choice is invalid. + Den valgte mulighed er ugyldig . + + + The collection is invalid. + Samlingen er ugyldig. + + + Please select a valid color. + Vælg venligst en gyldig farve. + + + Please select a valid country. + Vælg venligst et gyldigt land. + + + Please select a valid currency. + Vælg venligst en gyldig valuta. + + + Please choose a valid date interval. + Vælg venligst et gyldigt datointerval. + + + Please enter a valid date and time. + Vælg venligst en gyldig dato og tid. + + + Please enter a valid date. + Vælg venligst en gyldig dato. + + + Please select a valid file. + Vælg venligst en gyldig fil. + + + The hidden field is invalid. + Det skjulte felt er ugyldigt. + + + Please enter an integer. + Indsæt veligst et heltal. + + + Please select a valid language. + Vælg venligst et gyldigt sprog. + + + Please select a valid locale. + Vælg venligst en gyldigt sprogkode. + + + Please enter a valid money amount. + Vælg venligst et gyldigt beløb. + + + Please enter a number. + Indtast venligst et nummer. + + + The password is invalid. + Passwordet er ugyldigt. + + + Please enter a percentage value. + Indtast venligst en procentværdi. + + + The values do not match. + Værdierne er ikke ens. + + + Please enter a valid time. + Indtast venligst en gyldig tid. + + + Please select a valid timezone. + Vælg venligst en gyldig tidszone. + + + Please enter a valid URL. + Indtast venligst en gyldig URL. + + + Please enter a valid search term. + Indtast venligst et gyldigt søgeord. + + + Please provide a valid phone number. + Giv venligst et gyldigt telefonnummer. + + + The checkbox has an invalid value. + Checkboxen har en ugyldigt værdi. + + + Please enter a valid email address. + Indtast venligst en gyldig e-mailadresse. + + + Please select a valid option. + Vælg venligst en gyldig mulighed. + + + Please select a valid range. + Vælg venligst et gyldigt interval . + + + Please enter a valid week. + Indtast venligst en gyldig uge. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.de.xlf b/lib/symfony/form/Resources/translations/validators.de.xlf new file mode 100644 index 0000000000..759fa2a19c --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.de.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Dieses Formular sollte keine zusätzlichen Felder enthalten. + + + The uploaded file was too large. Please try to upload a smaller file. + Die hochgeladene Datei ist zu groß. Versuchen Sie bitte eine kleinere Datei hochzuladen. + + + The CSRF token is invalid. Please try to resubmit the form. + Der CSRF-Token ist ungültig. Versuchen Sie bitte, das Formular erneut zu senden. + + + This value is not a valid HTML5 color. + Dieser Wert ist keine gültige HTML5 Farbe. + + + Please enter a valid birthdate. + Bitte geben Sie ein gültiges Geburtsdatum ein. + + + The selected choice is invalid. + Die Auswahl ist ungültig. + + + The collection is invalid. + Diese Gruppe von Feldern ist ungültig. + + + Please select a valid color. + Bitte geben Sie eine gültige Farbe ein. + + + Please select a valid country. + Bitte wählen Sie ein gültiges Land aus. + + + Please select a valid currency. + Bitte wählen Sie eine gültige Währung aus. + + + Please choose a valid date interval. + Bitte wählen Sie ein gültiges Datumsintervall. + + + Please enter a valid date and time. + Bitte geben Sie ein gültiges Datum samt Uhrzeit ein. + + + Please enter a valid date. + Bitte geben Sie ein gültiges Datum ein. + + + Please select a valid file. + Bitte wählen Sie eine gültige Datei. + + + The hidden field is invalid. + Das versteckte Feld ist ungültig. + + + Please enter an integer. + Bitte geben Sie eine ganze Zahl ein. + + + Please select a valid language. + Bitte wählen Sie eine gültige Sprache. + + + Please select a valid locale. + Bitte wählen Sie eine gültige Locale-Einstellung aus. + + + Please enter a valid money amount. + Bitte geben Sie einen gültigen Geldbetrag ein. + + + Please enter a number. + Bitte geben Sie eine gültige Zahl ein. + + + The password is invalid. + Das Kennwort ist ungültig. + + + Please enter a percentage value. + Bitte geben Sie einen gültigen Prozentwert ein. + + + The values do not match. + Die Werte stimmen nicht überein. + + + Please enter a valid time. + Bitte geben Sie eine gültige Uhrzeit ein. + + + Please select a valid timezone. + Bitte wählen Sie eine gültige Zeitzone. + + + Please enter a valid URL. + Bitte geben Sie eine gültige URL ein. + + + Please enter a valid search term. + Bitte geben Sie einen gültigen Suchbegriff ein. + + + Please provide a valid phone number. + Bitte geben Sie eine gültige Telefonnummer ein. + + + The checkbox has an invalid value. + Das Kontrollkästchen hat einen ungültigen Wert. + + + Please enter a valid email address. + Bitte geben Sie eine gültige E-Mail-Adresse ein. + + + Please select a valid option. + Bitte wählen Sie eine gültige Option. + + + Please select a valid range. + Bitte wählen Sie einen gültigen Bereich. + + + Please enter a valid week. + Bitte geben Sie eine gültige Woche ein. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.el.xlf b/lib/symfony/form/Resources/translations/validators.el.xlf new file mode 100644 index 0000000000..b544dcbc61 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.el.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Αυτή η φόρμα δεν πρέπει να περιέχει επιπλέον πεδία. + + + The uploaded file was too large. Please try to upload a smaller file. + Το αρχείο είναι πολύ μεγάλο. Παρακαλούμε προσπαθήστε να ανεβάσετε ένα μικρότερο αρχείο. + + + The CSRF token is invalid. Please try to resubmit the form. + Το CSRF token δεν είναι έγκυρο. Παρακαλούμε δοκιμάστε να υποβάλετε τη φόρμα ξανά. + + + This value is not a valid HTML5 color. + Αυτή η τιμή δέν έναι έγκυρο χρώμα HTML5. + + + Please enter a valid birthdate. + Παρακαλόυμε ειχάγεται μία έγκυρη ημερομηνία γέννησης. + + + The selected choice is invalid. + Η επιλεγμένη επιλογή δέν είναι έγκυρη. + + + The collection is invalid. + Η συλλογή δέν είναι έγκυρη. + + + Please select a valid color. + Παρακαλούμε επιλέξτε ένα έγκυρο χρώμα. + + + Please select a valid country. + Παρακαλούμε επιλέξτε μία έγκυρη χώρα. + + + Please select a valid currency. + Παρακαλούμε επιλέξτε ένα έγυρο νόμισμα. + + + Please choose a valid date interval. + Παρακαλούμε επιλέξτε ένα έγκυρο διάστημα ημερομηνίας. + + + Please enter a valid date and time. + Παρακαλούμε εισαγάγετε μια έγκυρη ημερομηνία και ώρα. + + + Please enter a valid date. + Παρακαλούμε εισάγετε μία έγκυρη ημερομηνία. + + + Please select a valid file. + Παρακαλούμε επιλέξτε ένα έγκυρο αρχείο. + + + The hidden field is invalid. + Το κρυφό πεδίο δέν είναι έγκυρο. + + + Please enter an integer. + Παρακαλούμε εισάγετε έναν ακέραιο αριθμό. + + + Please select a valid language. + Παρακαλούμε επιλέξτε μία έγκυρη γλώσσα. + + + Please select a valid locale. + Παρακαλούμε επιλέξτε μία έγκυρη τοπικοποίηση. + + + Please enter a valid money amount. + Παρακαλούμε εισάγετε ένα έγκυρο χρηματικό ποσό. + + + Please enter a number. + Παρακαλούμε εισάγετε έναν αριθμό. + + + The password is invalid. + Ο κωδικός δέν είναι έγκυρος. + + + Please enter a percentage value. + Παρακαλούμε εισάγετε μία ποσοστιαία τιμή. + + + The values do not match. + Οι τιμές δέν ταιριάζουν. + + + Please enter a valid time. + Παρακαλούμε εισάγετε μία έγκυρη ώρα. + + + Please select a valid timezone. + Παρακαλούμε επιλέξτε μία έγυρη ζώνη ώρας. + + + Please enter a valid URL. + Παρακαλούμε εισάγετε μια έγκυρη διεύθυνση URL. + + + Please enter a valid search term. + Παρακαλούμε εισάγετε έναν έγκυρο όρο αναζήτησης. + + + Please provide a valid phone number. + Παρακαλούμε καταχωρίστε έναν έγκυρο αριθμό τηλεφώνου. + + + The checkbox has an invalid value. + Το πλαίσιο ελέγχου έχει μή έγκυρη τιμή. + + + Please enter a valid email address. + Παρακαλούμε εισάγετε μία έγκυρη ηλεκτρονική διεύθυνση. + + + Please select a valid option. + Παρακαλούμε επιλέξτε μία έγκυρη επιλογή. + + + Please select a valid range. + Παρακαλούμε επιλέξτε ένα έγυρο εύρος. + + + Please enter a valid week. + Παρακαλούμε εισάγετε μία έγκυρη εβδομάδα. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.en.xlf b/lib/symfony/form/Resources/translations/validators.en.xlf new file mode 100644 index 0000000000..57d3da969f --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.en.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + This form should not contain extra fields. + + + The uploaded file was too large. Please try to upload a smaller file. + The uploaded file was too large. Please try to upload a smaller file. + + + The CSRF token is invalid. Please try to resubmit the form. + The CSRF token is invalid. Please try to resubmit the form. + + + This value is not a valid HTML5 color. + This value is not a valid HTML5 color. + + + Please enter a valid birthdate. + Please enter a valid birthdate. + + + The selected choice is invalid. + The selected choice is invalid. + + + The collection is invalid. + The collection is invalid. + + + Please select a valid color. + Please select a valid color. + + + Please select a valid country. + Please select a valid country. + + + Please select a valid currency. + Please select a valid currency. + + + Please choose a valid date interval. + Please choose a valid date interval. + + + Please enter a valid date and time. + Please enter a valid date and time. + + + Please enter a valid date. + Please enter a valid date. + + + Please select a valid file. + Please select a valid file. + + + The hidden field is invalid. + The hidden field is invalid. + + + Please enter an integer. + Please enter an integer. + + + Please select a valid language. + Please select a valid language. + + + Please select a valid locale. + Please select a valid locale. + + + Please enter a valid money amount. + Please enter a valid money amount. + + + Please enter a number. + Please enter a number. + + + The password is invalid. + The password is invalid. + + + Please enter a percentage value. + Please enter a percentage value. + + + The values do not match. + The values do not match. + + + Please enter a valid time. + Please enter a valid time. + + + Please select a valid timezone. + Please select a valid timezone. + + + Please enter a valid URL. + Please enter a valid URL. + + + Please enter a valid search term. + Please enter a valid search term. + + + Please provide a valid phone number. + Please provide a valid phone number. + + + The checkbox has an invalid value. + The checkbox has an invalid value. + + + Please enter a valid email address. + Please enter a valid email address. + + + Please select a valid option. + Please select a valid option. + + + Please select a valid range. + Please select a valid range. + + + Please enter a valid week. + Please enter a valid week. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.es.xlf b/lib/symfony/form/Resources/translations/validators.es.xlf new file mode 100644 index 0000000000..a9989737c3 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.es.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Este formulario no debería contener campos adicionales. + + + The uploaded file was too large. Please try to upload a smaller file. + El archivo subido es demasiado grande. Por favor, suba un archivo más pequeño. + + + The CSRF token is invalid. Please try to resubmit the form. + El token CSRF no es válido. Por favor, pruebe a enviar nuevamente el formulario. + + + This value is not a valid HTML5 color. + Este valor no es un color HTML5 válido. + + + Please enter a valid birthdate. + Por favor, ingrese una fecha de cumpleaños válida. + + + The selected choice is invalid. + La opción seleccionada no es válida. + + + The collection is invalid. + La colección no es válida. + + + Please select a valid color. + Por favor, seleccione un color válido. + + + Please select a valid country. + Por favor, seleccione un país válido. + + + Please select a valid currency. + Por favor, seleccione una moneda válida. + + + Please choose a valid date interval. + Por favor, elija un intervalo de fechas válido. + + + Please enter a valid date and time. + Por favor, ingrese una fecha y hora válidas. + + + Please enter a valid date. + Por favor, ingrese una fecha válida. + + + Please select a valid file. + Por favor, seleccione un archivo válido. + + + The hidden field is invalid. + El campo oculto no es válido. + + + Please enter an integer. + Por favor, ingrese un número entero. + + + Please select a valid language. + Por favor, seleccione un idioma válido. + + + Please select a valid locale. + Por favor, seleccione una configuración regional válida. + + + Please enter a valid money amount. + Por favor, ingrese una cantidad de dinero válida. + + + Please enter a number. + Por favor, ingrese un número. + + + The password is invalid. + La contraseña no es válida. + + + Please enter a percentage value. + Por favor, ingrese un valor porcentual. + + + The values do not match. + Los valores no coinciden. + + + Please enter a valid time. + Por favor, ingrese una hora válida. + + + Please select a valid timezone. + Por favor, seleccione una zona horaria válida. + + + Please enter a valid URL. + Por favor, ingrese una URL válida. + + + Please enter a valid search term. + Por favor, ingrese un término de búsqueda válido. + + + Please provide a valid phone number. + Por favor, proporcione un número de teléfono válido. + + + The checkbox has an invalid value. + La casilla de verificación tiene un valor inválido. + + + Please enter a valid email address. + Por favor, ingrese una dirección de correo electrónico válida. + + + Please select a valid option. + Por favor, seleccione una opción válida. + + + Please select a valid range. + Por favor, seleccione un rango válido. + + + Please enter a valid week. + Por favor, ingrese una semana válida. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.et.xlf b/lib/symfony/form/Resources/translations/validators.et.xlf new file mode 100644 index 0000000000..0767220efa --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.et.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Väljade grupp ei tohiks sisalda lisaväljasid. + + + The uploaded file was too large. Please try to upload a smaller file. + Üleslaaditud fail oli liiga suur. Palun proovi uuesti väiksema failiga. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-märgis on vigane. Palun proovi vormi uuesti esitada. + + + This value is not a valid HTML5 color. + See väärtus ei ole korrektne HTML5 värv. + + + Please enter a valid birthdate. + Palun sisesta korrektne sünnikuupäev. + + + The selected choice is invalid. + Tehtud valik on vigane. + + + The collection is invalid. + Kogum on vigane. + + + Please select a valid color. + Palun vali korrektne värv. + + + Please select a valid country. + Palun vali korrektne riik. + + + Please select a valid currency. + Palun vali korrektne valuuta. + + + Please choose a valid date interval. + Palun vali korrektne kuupäevade vahemik. + + + Please enter a valid date and time. + Palun sisesta korrektne kuupäev ja kellaaeg. + + + Please enter a valid date. + Palun sisesta korrektne kuupäev. + + + Please select a valid file. + Palun vali korrektne fail. + + + The hidden field is invalid. + Peidetud väli on vigane. + + + Please enter an integer. + Palun sisesta täisarv. + + + Please select a valid language. + Palun vali korrektne keel. + + + Please select a valid locale. + Palun vali korrektne keelekood. + + + Please enter a valid money amount. + Palun sisesta korrektne rahaline väärtus. + + + Please enter a number. + Palun sisesta number. + + + The password is invalid. + Vigane parool. + + + Please enter a percentage value. + Palun sisesta protsendiline väärtus. + + + The values do not match. + Väärtused ei klapi. + + + Please enter a valid time. + Palun sisesta korrektne aeg. + + + Please select a valid timezone. + Palun vali korrektne ajavöönd. + + + Please enter a valid URL. + Palun sisesta korrektne URL. + + + Please enter a valid search term. + Palun sisesta korrektne otsingutermin. + + + Please provide a valid phone number. + Palun sisesta korrektne telefoninumber. + + + The checkbox has an invalid value. + Märkeruudu väärtus on vigane. + + + Please enter a valid email address. + Palun sisesta korrektne e-posti aadress. + + + Please select a valid option. + Palun tee korrektne valik. + + + Please select a valid range. + Palun vali korrektne vahemik. + + + Please enter a valid week. + Palun sisesta korrektne nädal. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.eu.xlf b/lib/symfony/form/Resources/translations/validators.eu.xlf new file mode 100644 index 0000000000..a73c63abb7 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.eu.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Formulario honek ez luke aparteko eremurik eduki behar. + + + The uploaded file was too large. Please try to upload a smaller file. + Igotako fitxategia handiegia da. Mesedez saiatu fitxategi txikiago bat igotzen. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF tokena baliogabea da. Mesedez, saiatu berriro formularioa bidaltzen. + + + This value is not a valid HTML5 color. + Balio hori ez da HTML5 kolore onargarria. + + + Please enter a valid birthdate. + Mesedez, sartu baliozko urtebetetze-eguna. + + + The selected choice is invalid. + Hautatutako aukera ez da egokia. + + + The collection is invalid. + Bilduma ez da baliozkoa. + + + Please select a valid color. + Mesedez, hautatu baliozko kolore bat. + + + Please select a valid country. + Mesedez, hautatu baliozko herrialde bat. + + + Please select a valid currency. + Mesedez, hautatu baliozko moneta bat. + + + Please choose a valid date interval. + Mesedez, hautatu baliozko data-tarte bat. + + + Please enter a valid date and time. + Mesedez, sartu baliozko data eta ordua. + + + Please enter a valid date. + Mesedez, sartu baliozko data bat. + + + Please select a valid file. + Mesedez, hautatu baliozko fitxategi bat. + + + The hidden field is invalid. + Eremu ezkutua ez da baliozkoa. + + + Please enter an integer. + Mesedez, sartu zenbaki oso bat. + + + Please select a valid language. + Mesedez, hautatu baliozko hizkuntza bat. + + + Please select a valid locale. + Mesedez, hautatu baliozko eskualde-konfigurazio bat. + + + Please enter a valid money amount. + Mesedez, sartu baliozko diru-kopuru bat. + + + Please enter a number. + Mesedez, sartu zenbaki bat. + + + The password is invalid. + Pasahitza ez da zuzena. + + + Please enter a percentage value. + Mesedez, sartu portzentajezko balio bat. + + + The values do not match. + Balioak ez datoz bat. + + + Please enter a valid time. + Mesedez, sartu baliozko ordu bat. + + + Please select a valid timezone. + Mesedez, hautatu baliozko ordu-eremua. + + + Please enter a valid URL. + Mesedez, sartu baliozko URL bat. + + + Please enter a valid search term. + Mesedez, sartu bilaketa-termino onargarri bat. + + + Please provide a valid phone number. + Mesedez, eman baliozko telefono-zenbaki bat. + + + The checkbox has an invalid value. + Egiaztatze-laukiak balio baliogabea du. + + + Please enter a valid email address. + Mesedez, sartu baliozko helbide elektroniko bat. + + + Please select a valid option. + Mesedez, hautatu baliozko aukera bat. + + + Please select a valid range. + Mesedez, hautatu baliozko tarte bat. + + + Please enter a valid week. + Mesedez, sartu baliozko aste bat. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.fa.xlf b/lib/symfony/form/Resources/translations/validators.fa.xlf new file mode 100644 index 0000000000..2ebb1cc2bb --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.fa.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + این فرم نباید شامل فیلدهای اضافی باشد. + + + The uploaded file was too large. Please try to upload a smaller file. + فایل بارگذاری‌شده بسیار بزرگ است. لطفاً فایل کوچک‌تری را بارگذاری نمایید. + + + The CSRF token is invalid. Please try to resubmit the form. + توکن CSRF نامعتبر است. لطفاً فرم را مجدداً ارسال نمایید. + + + This value is not a valid HTML5 color. + این مقدار یک رنگ معتبر HTML5 نیست. + + + Please enter a valid birthdate. + لطفاً یک تاریخ تولد معتبر وارد نمایید. + + + The selected choice is invalid. + گزینه‌ انتخاب‌ شده نامعتبر است. + + + The collection is invalid. + این مجموعه نامعتبر است. + + + Please select a valid color. + لطفاً یک رنگ معتبر انتخاب کنید. + + + Please select a valid country. + لطفاً یک کشور معتبر انتخاب کنید. + + + Please select a valid currency. + لطفاً یک واحد پول معتبر انتخاب کنید. + + + Please choose a valid date interval. + لطفاً یک بازه‌ زمانی معتبر انتخاب کنید. + + + Please enter a valid date and time. + لطفاً یک تاریخ و زمان معتبر وارد کنید. + + + Please enter a valid date. + لطفاً یک تاریخ معتبر وارد کنید. + + + Please select a valid file. + لطفاً یک فایل معتبر انتخاب کنید. + + + The hidden field is invalid. + فیلد مخفی نامعتبر است. + + + Please enter an integer. + لطفاً یک عدد صحیح وارد کنید. + + + Please select a valid language. + لطفاً یک زبان معتبر انتخاب کنید. + + + Please select a valid locale. + لطفاً یک منطقه‌جغرافیایی (locale) معتبر انتخاب کنید. + + + Please enter a valid money amount. + لطفاً یک مقدار پول معتبر وارد کنید. + + + Please enter a number. + لطفاً یک عدد وارد کنید. + + + The password is invalid. + رمزعبور نامعتبر است. + + + Please enter a percentage value. + لطفاً یک درصد معتبر وارد کنید. + + + The values do not match. + مقادیر تطابق ندارند. + + + Please enter a valid time. + لطفاً یک زمان معتبر وارد کنید. + + + Please select a valid timezone. + لطفاً یک منطقه‌زمانی معتبر وارد کنید. + + + Please enter a valid URL. + لطفاً یک URL معتبر وارد کنید. + + + Please enter a valid search term. + لطفاً یک عبارت جستجوی معتبر وارد کنید. + + + Please provide a valid phone number. + لطفاً یک شماره تلفن معتبر وارد کنید. + + + The checkbox has an invalid value. + کادر انتخاب (checkbox) دارای مقداری نامعتبر است. + + + Please enter a valid email address. + لطفاً یک آدرس رایانامه (ایمیل) معتبر وارد کنید. + + + Please select a valid option. + لطفاً یک گزینه‌ معتبر انتخاب کنید. + + + Please select a valid range. + لطفاً یک محدوده‌ معتبر انتخاب کنید. + + + Please enter a valid week. + لطفاً یک هفته‌ معتبر وارد کنید. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.fi.xlf b/lib/symfony/form/Resources/translations/validators.fi.xlf new file mode 100644 index 0000000000..438365404e --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.fi.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Tämä lomake ei voi sisältää ylimääräisiä kenttiä. + + + The uploaded file was too large. Please try to upload a smaller file. + Ladattu tiedosto on liian iso. Ole hyvä ja lataa pienempi tiedosto. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-tarkiste on virheellinen. Ole hyvä ja yritä lähettää lomake uudestaan. + + + This value is not a valid HTML5 color. + Tämä arvo ei ole kelvollinen HTML5-väri. + + + Please enter a valid birthdate. + Syötä kelvollinen syntymäaika. + + + The selected choice is invalid. + Valittu vaihtoehto ei kelpaa. + + + The collection is invalid. + Ryhmä ei kelpaa. + + + Please select a valid color. + Valitse kelvollinen väri. + + + Please select a valid country. + Valitse kelvollinen maa. + + + Please select a valid currency. + Valitse kelvollinen valuutta. + + + Please choose a valid date interval. + Valitse kelvollinen aikaväli. + + + Please enter a valid date and time. + Syötä kelvolliset päivä ja aika. + + + Please enter a valid date. + Syötä kelvollinen päivä. + + + Please select a valid file. + Valitse kelvollinen tiedosto. + + + The hidden field is invalid. + Piilotettu kenttä ei ole kelvollinen. + + + Please enter an integer. + Syötä kokonaisluku. + + + Please select a valid language. + Valitse kelvollinen kieli. + + + Please select a valid locale. + Valitse kelvollinen kielikoodi. + + + Please enter a valid money amount. + Syötä kelvollinen rahasumma. + + + Please enter a number. + Syötä numero. + + + The password is invalid. + Salasana ei kelpaa. + + + Please enter a percentage value. + Syötä prosenttiluku. + + + The values do not match. + Arvot eivät vastaa toisiaan. + + + Please enter a valid time. + Syötä kelvollinen kellonaika. + + + Please select a valid timezone. + Valitse kelvollinen aikavyöhyke. + + + Please enter a valid URL. + Syötä kelvollinen URL. + + + Please enter a valid search term. + Syötä kelvollinen hakusana. + + + Please provide a valid phone number. + Anna kelvollinen puhelinnumero. + + + The checkbox has an invalid value. + Valintaruudun arvo ei kelpaa. + + + Please enter a valid email address. + Syötä kelvollinen sähköpostiosoite. + + + Please select a valid option. + Valitse kelvollinen vaihtoehto. + + + Please select a valid range. + Valitse kelvollinen väli. + + + Please enter a valid week. + Syötä kelvollinen viikko. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.fr.xlf b/lib/symfony/form/Resources/translations/validators.fr.xlf new file mode 100644 index 0000000000..cbfb4f83cd --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.fr.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ce formulaire ne doit pas contenir de champs supplémentaires. + + + The uploaded file was too large. Please try to upload a smaller file. + Le fichier téléchargé est trop volumineux. Merci d'essayer d'envoyer un fichier plus petit. + + + The CSRF token is invalid. Please try to resubmit the form. + Le jeton CSRF est invalide. Veuillez renvoyer le formulaire. + + + This value is not a valid HTML5 color. + Cette valeur n'est pas une couleur HTML5 valide. + + + Please enter a valid birthdate. + Veuillez entrer une date de naissance valide. + + + The selected choice is invalid. + Le choix sélectionné est invalide. + + + The collection is invalid. + La collection est invalide. + + + Please select a valid color. + Veuillez sélectionner une couleur valide. + + + Please select a valid country. + Veuillez sélectionner un pays valide. + + + Please select a valid currency. + Veuillez sélectionner une devise valide. + + + Please choose a valid date interval. + Veuillez choisir un intervalle de dates valide. + + + Please enter a valid date and time. + Veuillez saisir une date et une heure valides. + + + Please enter a valid date. + Veuillez entrer une date valide. + + + Please select a valid file. + Veuillez sélectionner un fichier valide. + + + The hidden field is invalid. + Le champ masqué n'est pas valide. + + + Please enter an integer. + Veuillez saisir un entier. + + + Please select a valid language. + Veuillez sélectionner une langue valide. + + + Please select a valid locale. + Veuillez sélectionner une langue valide. + + + Please enter a valid money amount. + Veuillez saisir un montant valide. + + + Please enter a number. + Veuillez saisir un nombre. + + + The password is invalid. + Le mot de passe est invalide. + + + Please enter a percentage value. + Veuillez saisir un pourcentage valide. + + + The values do not match. + Les valeurs ne correspondent pas. + + + Please enter a valid time. + Veuillez saisir une heure valide. + + + Please select a valid timezone. + Veuillez sélectionner un fuseau horaire valide. + + + Please enter a valid URL. + Veuillez saisir une URL valide. + + + Please enter a valid search term. + Veuillez saisir un terme de recherche valide. + + + Please provide a valid phone number. + Veuillez fournir un numéro de téléphone valide. + + + The checkbox has an invalid value. + La case à cocher a une valeur non valide. + + + Please enter a valid email address. + Veuillez saisir une adresse email valide. + + + Please select a valid option. + Veuillez sélectionner une option valide. + + + Please select a valid range. + Veuillez sélectionner une plage valide. + + + Please enter a valid week. + Veuillez entrer une semaine valide. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.gl.xlf b/lib/symfony/form/Resources/translations/validators.gl.xlf new file mode 100644 index 0000000000..e3427f8d28 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.gl.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Este formulario non debería conter campos adicionais. + + + The uploaded file was too large. Please try to upload a smaller file. + O arquivo subido é demasiado grande. Por favor, suba un arquivo máis pequeno. + + + The CSRF token is invalid. Please try to resubmit the form. + O token CSRF non é válido. Por favor, probe a enviar novamente o formulario. + + + This value is not a valid HTML5 color. + Este valor non é unha cor HTML5 válida. + + + Please enter a valid birthdate. + Insire unha data de aniversario válida. + + + The selected choice is invalid. + A opción seleccionada non é válida. + + + The collection is invalid. + A colección non é válida. + + + Please select a valid color. + Por favor, seleccione unha cor válida. + + + Please select a valid country. + Por favor, seleccione un país válido. + + + Please select a valid currency. + Por favor, seleccione unha moeda válida. + + + Please choose a valid date interval. + Por favor, escolla un intervalo de datas válido. + + + Please enter a valid date and time. + Por favor, introduza unha data e hora válidas. + + + Please enter a valid date. + Por favor, introduce unha data válida. + + + Please select a valid file. + Por favor, seleccione un ficheiro válido. + + + The hidden field is invalid. + O campo oculto non é válido. + + + Please enter an integer. + Por favor, introduza un número enteiro. + + + Please select a valid language. + Por favor, selecciona un idioma válido. + + + Please select a valid locale. + Por favor, seleccione unha configuración rexional válida. + + + Please enter a valid money amount. + Por favor, introduza unha cantidade de diñeiro válida. + + + Please enter a number. + Por favor, introduza un número. + + + The password is invalid. + O contrasinal non é válido. + + + Please enter a percentage value. + Por favor, introduza un valor porcentual. + + + The values do not match. + Os valores non coinciden. + + + Please enter a valid time. + Por favor, introduza unha hora válida. + + + Please select a valid timezone. + Por favor, selecciona unha zona horaria válida. + + + Please enter a valid URL. + Por favor, introduce un URL válido. + + + Please enter a valid search term. + Por favor, introduce un termo de busca válido. + + + Please provide a valid phone number. + Por favor, fornecer un número de teléfono válido. + + + The checkbox has an invalid value. + A caixa de verificación ten un valor non válido. + + + Please enter a valid email address. + Por favor, introduce un enderezo de correo electrónico válido. + + + Please select a valid option. + Por favor, seleccione unha opción válida. + + + Please select a valid range. + Por favor, seleccione un intervalo válido. + + + Please enter a valid week. + Por favor, introduce unha semana válida. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.he.xlf b/lib/symfony/form/Resources/translations/validators.he.xlf new file mode 100644 index 0000000000..41428ac70f --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.he.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + הטופס לא צריך להכיל שדות נוספים. + + + The uploaded file was too large. Please try to upload a smaller file. + הקובץ שהועלה גדול מדי. נסה להעלות קובץ קטן יותר. + + + The CSRF token is invalid. Please try to resubmit the form. + אסימון CSRF אינו חוקי. אנא נסה לשלוח שוב את הטופס. + + + This value is not a valid HTML5 color. + ערך זה אינו צבע HTML5 חוקי. + + + Please enter a valid birthdate. + נא להזין את תאריך לידה תקני. + + + The selected choice is invalid. + הבחירה שנבחרה אינה חוקית. + + + The collection is invalid. + האוסף אינו חוקי. + + + Please select a valid color. + אנא בחר צבע חוקי. + + + Please select a valid country. + אנא בחר מדינה חוקית. + + + Please select a valid currency. + אנא בחר מטבע חוקי. + + + Please choose a valid date interval. + אנא בחר מרווח תאריכים חוקי. + + + Please enter a valid date and time. + אנא הזן תאריך ושעה תקנים. + + + Please enter a valid date. + נא להזין תאריך חוקי. + + + Please select a valid file. + אנא בחר קובץ חוקי. + + + The hidden field is invalid. + השדה הנסתר אינו חוקי. + + + Please enter an integer. + אנא הזן מספר שלם. + + + Please select a valid language. + אנא בחר שפה חוקי. + + + Please select a valid locale. + אנא בחר שפה מקומית. + + + Please enter a valid money amount. + אנא הזן סכום כסף חוקי. + + + Please enter a number. + אנא הזן מספר. + + + The password is invalid. + הסיסמה אינה חוקית. + + + Please enter a percentage value. + אנא הזן ערך באחוזים. + + + The values do not match. + הערכים אינם תואמים. + + + Please enter a valid time. + אנא הזן שעה חוקי. + + + Please select a valid timezone. + אנא בחר אזור זמן חוקי. + + + Please enter a valid URL. + נא להזין את כתובת אתר חוקית. + + + Please enter a valid search term. + אנא הזן מונח חיפוש חוקי. + + + Please provide a valid phone number. + אנא ספק מספר טלפון חוקי. + + + The checkbox has an invalid value. + לתיבת הסימון יש ערך לא חוקי. + + + Please enter a valid email address. + אנא הזן כתובת דוא"ל תקנית. + + + Please select a valid option. + אנא בחר אפשרות חוקית. + + + Please select a valid range. + אנא בחר טווח חוקי. + + + Please enter a valid week. + אנא הזן שבוע תקף. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.hr.xlf b/lib/symfony/form/Resources/translations/validators.hr.xlf new file mode 100644 index 0000000000..e3aa7b2b9c --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.hr.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ovaj obrazac ne smije sadržavati dodatna polja. + + + The uploaded file was too large. Please try to upload a smaller file. + Prenesena datoteka je prevelika. Molim pokušajte prenijeti manju datoteku. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF vrijednost nije ispravna. Pokušajte ponovo poslati obrazac. + + + This value is not a valid HTML5 color. + Ova vrijednost nije važeća HTML5 boja. + + + Please enter a valid birthdate. + Molim upišite ispravan datum rođenja. + + + The selected choice is invalid. + Odabrani izbor nije ispravan. + + + The collection is invalid. + Kolekcija nije ispravna. + + + Please select a valid color. + Molim odaberite ispravnu boju. + + + Please select a valid country. + Molim odaberite ispravnu državu. + + + Please select a valid currency. + Molim odaberite ispravnu valutu. + + + Please choose a valid date interval. + Molim odaberite ispravni vremenski interval. + + + Please enter a valid date and time. + Molim unesite ispravni datum i vrijeme. + + + Please enter a valid date. + Molim odaberite ispravan datum. + + + Please select a valid file. + Molim odaberite ispravnu datoteku. + + + The hidden field is invalid. + Skriveno polje nije ispravno. + + + Please enter an integer. + Molim unesite cijeli broj. + + + Please select a valid language. + Molim odaberite ispravan jezik. + + + Please select a valid locale. + Molim odaberite ispravnu lokalizaciju. + + + Please enter a valid money amount. + Molim unesite ispravan iznos novca. + + + Please enter a number. + Molim unesite broj. + + + The password is invalid. + Ova lozinka nije ispravna. + + + Please enter a percentage value. + Molim unesite vrijednost postotka. + + + The values do not match. + Ove vrijednosti se ne poklapaju. + + + Please enter a valid time. + Molim unesite ispravno vrijeme. + + + Please select a valid timezone. + Molim odaberite ispravnu vremensku zonu. + + + Please enter a valid URL. + Molim unesite ispravan URL. + + + Please enter a valid search term. + Molim unesite ispravan pojam za pretraživanje. + + + Please provide a valid phone number. + Molim navedite ispravan telefonski broj. + + + The checkbox has an invalid value. + Polje za potvrdu sadrži neispravnu vrijednost. + + + Please enter a valid email address. + Molim unesite valjanu adresu elektronske pošte. + + + Please select a valid option. + Molim odaberite ispravnu opciju. + + + Please select a valid range. + Molim odaberite ispravan raspon. + + + Please enter a valid week. + Molim unesite ispravni tjedan. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.hu.xlf b/lib/symfony/form/Resources/translations/validators.hu.xlf new file mode 100644 index 0000000000..0ea74fea91 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.hu.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ez a mezőcsoport nem tartalmazhat extra mezőket. + + + The uploaded file was too large. Please try to upload a smaller file. + A feltöltött fájl túl nagy. Kérem, próbáljon egy kisebb fájlt feltölteni. + + + The CSRF token is invalid. Please try to resubmit the form. + Érvénytelen CSRF token. Kérem, próbálja újra elküldeni az űrlapot. + + + This value is not a valid HTML5 color. + Ez az érték nem egy érvényes HTML5 szín. + + + Please enter a valid birthdate. + Kérjük, adjon meg egy valós születési dátumot. + + + The selected choice is invalid. + A kiválasztott opció érvénytelen. + + + The collection is invalid. + A gyűjtemény érvénytelen. + + + Please select a valid color. + Kérjük, válasszon egy érvényes színt. + + + Please select a valid country. + Kérjük, válasszon egy érvényes országot. + + + Please select a valid currency. + Kérjük, válasszon egy érvényes pénznemet. + + + Please choose a valid date interval. + Kérjük, válasszon egy érvényes dátumintervallumot. + + + Please enter a valid date and time. + Kérjük, adjon meg egy érvényes dátumot és időpontot. + + + Please enter a valid date. + Kérjük, adjon meg egy érvényes dátumot. + + + Please select a valid file. + Kérjük, válasszon egy érvényes fájlt. + + + The hidden field is invalid. + A rejtett mező érvénytelen. + + + Please enter an integer. + Kérjük, adjon meg egy egész számot. + + + Please select a valid language. + Kérjük, válasszon egy érvényes nyelvet. + + + Please select a valid locale. + Kérjük, válasszon egy érvényes területi beállítást. + + + Please enter a valid money amount. + Kérjük, adjon meg egy érvényes pénzösszeget. + + + Please enter a number. + Kérjük, adjon meg egy számot. + + + The password is invalid. + A jelszó érvénytelen. + + + Please enter a percentage value. + Kérjük, adjon meg egy százalékos értéket. + + + The values do not match. + Az értékek nem egyeznek. + + + Please enter a valid time. + Kérjük, adjon meg egy érvényes időpontot. + + + Please select a valid timezone. + Kérjük, válasszon érvényes időzónát. + + + Please enter a valid URL. + Kérjük, adjon meg egy érvényes URL-t. + + + Please enter a valid search term. + Kérjük, adjon meg egy érvényes keresési kifejezést. + + + Please provide a valid phone number. + Kérjük, adjon egy érvényes telefonszámot + + + The checkbox has an invalid value. + A jelölőnégyzet értéke érvénytelen. + + + Please enter a valid email address. + Kérjük valós e-mail címet adjon meg. + + + Please select a valid option. + Kérjük, válasszon egy érvényes beállítást. + + + Please select a valid range. + Kérjük, válasszon egy érvényes tartományt. + + + Please enter a valid week. + Kérjük, adjon meg egy érvényes hetet. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.hy.xlf b/lib/symfony/form/Resources/translations/validators.hy.xlf new file mode 100644 index 0000000000..ccca247353 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.hy.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Այս ձևը չպետք է պարունակի լրացուցիչ տողեր։ + + + The uploaded file was too large. Please try to upload a smaller file. + Վերբեռնված ֆայլը չափազանց մեծ է. Խնդրվում է վերբեռնել ավելի փոքր չափսի ֆայլ։ + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF արժեքը անթույլատրելի է. Փորձեք նորից ուղարկել ձևը։ + + + This value is not a valid HTML5 color. + Այս արժեքը վավեր HTML5 գույն չէ։ + + + Please enter a valid birthdate. + Խնդրում ենք մուտքագրել վավեր ծննդյան ամսաթիվ։ + + + The selected choice is invalid. + Ընտրված ընտրությունն անվավեր է։ + + + The collection is invalid. + Համախումբն անվավեր է։ + + + Please select a valid color. + Խնդրում ենք ընտրել վավեր գույն։ + + + Please select a valid country. + Խնդրում ենք ընտրել վավեր երկիր։ + + + Please select a valid currency. + Խնդրում ենք ընտրել վավեր արժույթ։ + + + Please choose a valid date interval. + Խնդրում ենք ընտրել ճիշտ ամսաթվերի միջակայք։ + + + Please enter a valid date and time. + Խնդրում ենք մուտքագրել վավեր ամսաթիվ և ժամ։ + + + Please enter a valid date. + Խնդրում ենք մուտքագրել վավեր ամսաթիվ։ + + + Please select a valid file. + Խնդրում ենք ընտրել վավեր ֆայլ։ + + + The hidden field is invalid. + Թաքնված դաշտը անվավեր է։ + + + Please enter an integer. + Խնդրում ենք մուտքագրել ամբողջ թիվ։ + + + Please select a valid language. + Խնդրում ենք ընտրել վավեր լեզու։ + + + Please select a valid locale. + Խնդրում ենք ընտրել վավեր տեղայնացում։ + + + Please enter a valid money amount. + Խնդրում ենք մուտքագրել վավեր գումար։ + + + Please enter a number. + Խնդրում ենք մուտքագրել համար։ + + + The password is invalid. + Գաղտնաբառն անվավեր է։ + + + Please enter a percentage value. + Խնդրում ենք մուտքագրել տոկոսային արժեք։ + + + The values do not match. + Արժեքները չեն համընկնում։ + + + Please enter a valid time. + Մուտքագրեք վավեր ժամանակ։ + + + Please select a valid timezone. + Խնդրում ենք ընտրել վավեր ժամային գոտի։ + + + Please enter a valid URL. + Խնդրում ենք մուտքագրել վավեր URL։ + + + Please enter a valid search term. + Խնդրում ենք մուտքագրել վավեր որոնման տերմին։ + + + Please provide a valid phone number. + Խնդրում ենք տրամադրել վավեր հեռախոսահամար։ + + + The checkbox has an invalid value. + Նշման վանդակը անվավեր արժեք ունի։ + + + Please enter a valid email address. + Խնդրում ենք մուտքագրել վավեր էլ-հասցե։ + + + Please select a valid option. + Խնդրում ենք ընտրել ճիշտ տարբերակ։ + + + Please select a valid range. + Խնդրում ենք ընտրել վավեր տիրույթ։ + + + Please enter a valid week. + Մուտքագրեք վավեր շաբաթ։ + + + + diff --git a/lib/symfony/form/Resources/translations/validators.id.xlf b/lib/symfony/form/Resources/translations/validators.id.xlf new file mode 100644 index 0000000000..e4b43f7e3a --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.id.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Gabungan kolom tidak boleh mengandung kolom tambahan. + + + The uploaded file was too large. Please try to upload a smaller file. + Berkas yang di unggah terlalu besar. Silahkan coba unggah berkas yang lebih kecil. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-Token tidak sah. Silahkan coba kirim ulang formulir. + + + This value is not a valid HTML5 color. + Nilai ini bukan merupakan HTML5 color yang sah. + + + Please enter a valid birthdate. + Silahkan masukkan tanggal lahir yang sah. + + + The selected choice is invalid. + Pilihan yang dipilih tidak sah. + + + The collection is invalid. + Koleksi tidak sah. + + + Please select a valid color. + Silahkan pilih warna yang sah. + + + Please select a valid country. + Silahkan pilih negara yang sah. + + + Please select a valid currency. + Silahkan pilih mata uang yang sah. + + + Please choose a valid date interval. + Silahkan pilih interval tanggal yang sah. + + + Please enter a valid date and time. + Silahkan masukkan tanggal dan waktu yang sah. + + + Please enter a valid date. + Silahkan masukkan tanggal yang sah. + + + Please select a valid file. + Silahkan pilih berkas yang sah. + + + The hidden field is invalid. + Ruas yang tersembunyi tidak sah. + + + Please enter an integer. + Silahkan masukkan angka. + + + Please select a valid language. + Silahlan pilih bahasa yang sah. + + + Please select a valid locale. + Silahkan pilih local yang sah. + + + Please enter a valid money amount. + Silahkan masukkan nilai uang yang sah. + + + Please enter a number. + Silahkan masukkan sebuah angka + + + The password is invalid. + Kata sandi tidak sah. + + + Please enter a percentage value. + Silahkan masukkan sebuah nilai persentase. + + + The values do not match. + Nilainya tidak cocok. + + + Please enter a valid time. + Silahkan masukkan waktu yang sah. + + + Please select a valid timezone. + Silahkan pilih zona waktu yang sah. + + + Please enter a valid URL. + Silahkan masukkan URL yang sah. + + + Please enter a valid search term. + Silahkan masukkan kata pencarian yang sah. + + + Please provide a valid phone number. + Silahkan sediakan nomor telepon yang sah. + + + The checkbox has an invalid value. + Nilai dari checkbox tidak sah. + + + Please enter a valid email address. + Silahkan masukkan alamat surel yang sah. + + + Please select a valid option. + Silahkan pilih opsi yang sah. + + + Please select a valid range. + Silahkan pilih rentang yang sah. + + + Please enter a valid week. + Silahkan masukkan minggu yang sah. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.it.xlf b/lib/symfony/form/Resources/translations/validators.it.xlf new file mode 100644 index 0000000000..bdea7132f5 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.it.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Questo form non dovrebbe contenere nessun campo extra. + + + The uploaded file was too large. Please try to upload a smaller file. + Il file caricato è troppo grande. Per favore, carica un file più piccolo. + + + The CSRF token is invalid. Please try to resubmit the form. + Il token CSRF non è valido. Prova a reinviare il form. + + + This value is not a valid HTML5 color. + Il valore non è un colore HTML5 valido. + + + Please enter a valid birthdate. + Per favore, inserisci una data di compleanno valida. + + + The selected choice is invalid. + La scelta selezionata non è valida. + + + The collection is invalid. + La collezione non è valida. + + + Please select a valid color. + Per favore, seleziona un colore valido. + + + Please select a valid country. + Per favore, seleziona un paese valido. + + + Please select a valid currency. + Per favore, seleziona una valuta valida. + + + Please choose a valid date interval. + Per favore, scegli un intervallo di date valido. + + + Please enter a valid date and time. + Per favore, inserisci una data e ora valida. + + + Please enter a valid date. + Per favore, inserisci una data valida. + + + Please select a valid file. + Per favore, seleziona un file valido. + + + The hidden field is invalid. + Il campo nascosto non è valido. + + + Please enter an integer. + Per favore, inserisci un numero intero. + + + Please select a valid language. + Per favore, seleziona una lingua valida. + + + Please select a valid locale. + Per favore, seleziona una lingua valida. + + + Please enter a valid money amount. + Per favore, inserisci un importo valido. + + + Please enter a number. + Per favore, inserisci un numero. + + + The password is invalid. + La password non è valida. + + + Please enter a percentage value. + Per favore, inserisci un valore percentuale. + + + The values do not match. + I valori non corrispondono. + + + Please enter a valid time. + Per favore, inserisci un orario valido. + + + Please select a valid timezone. + Per favore, seleziona un fuso orario valido. + + + Please enter a valid URL. + Per favore, inserisci un URL valido. + + + Please enter a valid search term. + Per favore, inserisci un termine di ricerca valido. + + + Please provide a valid phone number. + Per favore, indica un numero di telefono valido. + + + The checkbox has an invalid value. + La casella di selezione non ha un valore valido. + + + Please enter a valid email address. + Per favore, indica un indirizzo email valido. + + + Please select a valid option. + Per favore, seleziona un'opzione valida. + + + Please select a valid range. + Per favore, seleziona un intervallo valido. + + + Please enter a valid week. + Per favore, inserisci una settimana valida. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.ja.xlf b/lib/symfony/form/Resources/translations/validators.ja.xlf new file mode 100644 index 0000000000..5728d9b1d4 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.ja.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + フィールドグループに追加のフィールドを含んではなりません。 + + + The uploaded file was too large. Please try to upload a smaller file. + アップロードされたファイルが大きすぎます。小さなファイルで再度アップロードしてください。 + + + The CSRF token is invalid. Please try to resubmit the form. + CSRFトークンが無効です、再送信してください。 + + + This value is not a valid HTML5 color. + 有効なHTML5の色ではありません。 + + + Please enter a valid birthdate. + 有効な生年月日を入力してください。 + + + The selected choice is invalid. + 選択した値は無効です。 + + + The collection is invalid. + コレクションは無効です。 + + + Please select a valid color. + 有効な色を選択してください。 + + + Please select a valid country. + 有効な国を選択してください。 + + + Please select a valid currency. + 有効な通貨を選択してください。 + + + Please choose a valid date interval. + 有効な日付間隔を選択してください。 + + + Please enter a valid date and time. + 有効な日時を入力してください。 + + + Please enter a valid date. + 有効な日付を入力してください。 + + + Please select a valid file. + 有効なファイルを選択してください。 + + + The hidden field is invalid. + 隠しフィールドが無効です。 + + + Please enter an integer. + 整数で入力してください。 + + + Please select a valid language. + 有効な言語を選択してください。 + + + Please select a valid locale. + 有効なロケールを選択してください。 + + + Please enter a valid money amount. + 有効な金額を入力してください。 + + + Please enter a number. + 数値で入力してください。 + + + The password is invalid. + パスワードが無効です。 + + + Please enter a percentage value. + パーセント値で入力してください。 + + + The values do not match. + 値が一致しません。 + + + Please enter a valid time. + 有効な時間を入力してください。 + + + Please select a valid timezone. + 有効なタイムゾーンを選択してください。 + + + Please enter a valid URL. + 有効なURLを入力してください。 + + + Please enter a valid search term. + 有効な検索語を入力してください。 + + + Please provide a valid phone number. + 有効な電話番号を入力してください。 + + + The checkbox has an invalid value. + チェックボックスの値が無効です。 + + + Please enter a valid email address. + 有効なメールアドレスを入力してください。 + + + Please select a valid option. + 有効な値を選択してください。 + + + Please select a valid range. + 有効な範囲を選択してください。 + + + Please enter a valid week. + 有効な週を入力してください。 + + + + diff --git a/lib/symfony/form/Resources/translations/validators.lb.xlf b/lib/symfony/form/Resources/translations/validators.lb.xlf new file mode 100644 index 0000000000..1f4ee820b2 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.lb.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Dës Feldergrupp sollt keng zousätzlech Felder enthalen. + + + The uploaded file was too large. Please try to upload a smaller file. + De geschécktene Fichier ass ze grouss. Versicht wann ech gelift ee méi klenge Fichier eropzelueden. + + + The CSRF token is invalid. Please try to resubmit the form. + Den CSRF-Token ass ongëlteg. Versicht wann ech gelift de Formulaire nach eng Kéier ze schécken. + + + This value is not a valid HTML5 color. + Dëse Wäert ass keng gëlteg HTML5-Faarf. + + + Please enter a valid birthdate. + W.e.g. e gëltege Gebuertsdatum aginn. + + + The selected choice is invalid. + Den ausgewielte Choix ass ongëlteg. + + + The collection is invalid. + D'Kollektioun ass ongëlteg. + + + Please select a valid color. + W.e.g. eng gëlteg Faarf auswielen. + + + Please select a valid country. + W.e.g. e gëltegt Land auswielen. + + + Please select a valid currency. + W.e.g. eng gëlteg Wärung auswielen. + + + Please choose a valid date interval. + W.e.g. e gëltegen Datumsinterval aginn. + + + Please enter a valid date and time. + W.e.g. eng gëlteg Datum an Zäit aginn. + + + Please enter a valid date. + W.e.g. eng gëltegen Datum aginn. + + + Please select a valid file. + W.e.g. e gëltege Fichier auswielen. + + + The hidden field is invalid. + Dat verstoppte Feld ass ongëlteg. + + + Please enter an integer. + W.e.g. eng ganz Zuel aginn. + + + Please select a valid language. + W.e.g. e gëltegt Sprooch auswielen. + + + Please select a valid locale. + W.e.g. e gëltegt Regionalschema auswielen. + + + Please enter a valid money amount. + W.e.g. eng gëlteg Geldzomm aginn. + + + Please enter a number. + W.e.g. eng Zuel aginn. + + + The password is invalid. + D'Passwuert ass ongëlteg. + + + Please enter a percentage value. + W.e.g. e Prozentwäert aginn. + + + The values do not match. + D'Wäerter stëmmen net iwwereneen. + + + Please enter a valid time. + W.e.g. eng gëlteg Zäit aginn. + + + Please select a valid timezone. + W.e.g. eng gëlteg Zäitzon auswielen. + + + Please enter a valid URL. + W.e.g. eng gëlteg URL aginn. + + + Please enter a valid search term. + W.e.g. e gëltege Sichbegrëff aginn. + + + Please provide a valid phone number. + W.e.g. eng gëlteg Telefonsnummer uginn. + + + The checkbox has an invalid value. + D'Ukräizfeld huet en ongëltege Wäert. + + + Please enter a valid email address. + W.e.g. eng gëlteg E-Mail-Adress aginn. + + + Please select a valid option. + W.e.g. eng gëlteg Optioun auswielen. + + + Please select a valid range. + W.e.g. eng gëlteg Spannbreet auswielen. + + + Please enter a valid week. + W.e.g. eng gëlteg Woch aginn. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.lt.xlf b/lib/symfony/form/Resources/translations/validators.lt.xlf new file mode 100644 index 0000000000..aba1120e3e --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.lt.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Forma negali turėti papildomų laukų. + + + The uploaded file was too large. Please try to upload a smaller file. + Įkelta byla yra per didelė. bandykite įkelti mažesnę. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF kodas nepriimtinas. Bandykite siųsti formos užklausą dar kartą. + + + This value is not a valid HTML5 color. + Ši reikšmė nėra HTML5 spalva. + + + Please enter a valid birthdate. + Prašome įvesti tinkamą gimimo datą. + + + The selected choice is invalid. + Pasirinktas pasirinkimas yra neteisingas. + + + The collection is invalid. + Neteisingas sąrašas. + + + Please select a valid color. + Prašome pasirinkti tinkamą spalvą. + + + Please select a valid country. + Prašome pasirinkti tinkamą šalį. + + + Please select a valid currency. + Prašome pasirinkti tinkamą valiutą. + + + Please choose a valid date interval. + Prašome pasirinkti tinkamą datos intervalą. + + + Please enter a valid date and time. + Prašome įvesti tinkamą datą ir laiką. + + + Please enter a valid date. + Prašome įvesti tinkamą datą. + + + Please select a valid file. + Prašome pasirinkti tinkamą bylą. + + + The hidden field is invalid. + Klaidingas paslėptasis laukas. + + + Please enter an integer. + Prašome įvesti sveiką skaičių. + + + Please select a valid language. + Prašome pasirinkti tinkamą kalbą. + + + Please select a valid locale. + Prašome pasirinkti tinkamą lokalę. + + + Please enter a valid money amount. + Prašome įvesti tinkamą pinigų sumą. + + + Please enter a number. + Prašome įvesti numerį. + + + The password is invalid. + Klaidingas slaptažodis. + + + Please enter a percentage value. + Prašome įvesti procentinę reikšmę. + + + The values do not match. + Reikšmės nesutampa. + + + Please enter a valid time. + Prašome įvesti tinkamą laiką. + + + Please select a valid timezone. + Prašome pasirinkti tinkamą laiko zoną. + + + Please enter a valid URL. + Prašome įvesti tinkamą URL. + + + Please enter a valid search term. + Prašome įvesti tinkamą paieškos terminą. + + + Please provide a valid phone number. + Prašome pateikti tinkamą telefono numerį. + + + The checkbox has an invalid value. + Klaidinga žymimajo langelio reikšmė. + + + Please enter a valid email address. + Prašome įvesti tinkamą el. pašto adresą. + + + Please select a valid option. + Prašome pasirinkti tinkamą parinktį. + + + Please select a valid range. + Prašome pasirinkti tinkamą diapozoną. + + + Please enter a valid week. + Prašome įvesti tinkamą savaitę. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.lv.xlf b/lib/symfony/form/Resources/translations/validators.lv.xlf new file mode 100644 index 0000000000..fb358dccf2 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.lv.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Šajā veidlapā nevajadzētu būt papildus ievades laukiem. + + + The uploaded file was too large. Please try to upload a smaller file. + Augšupielādētā faila izmērs bija par lielu. Lūdzu mēģiniet augšupielādēt mazāka izmēra failu. + + + The CSRF token is invalid. Please try to resubmit the form. + Dotais CSRF talons nav derīgs. Lūdzu mēģiniet vēlreiz iesniegt veidlapu. + + + This value is not a valid HTML5 color. + Šī vertība nav derīga HTML5 krāsa. + + + Please enter a valid birthdate. + Lūdzu, ievadiet derīgu dzimšanas datumu. + + + The selected choice is invalid. + Iezīmētā izvēle nav derīga. + + + The collection is invalid. + Kolekcija nav derīga. + + + Please select a valid color. + Lūdzu, izvēlieties derīgu krāsu. + + + Please select a valid country. + Lūdzu, izvēlieties derīgu valsti. + + + Please select a valid currency. + Lūdzu, izvēlieties derīgu valūtu. + + + Please choose a valid date interval. + Lūdzu, izvēlieties derīgu datumu intervālu. + + + Please enter a valid date and time. + Lūdzu, ievadiet derīgu datumu un laiku. + + + Please enter a valid date. + Lūdzu, ievadiet derīgu datumu. + + + Please select a valid file. + Lūdzu, izvēlieties derīgu failu. + + + The hidden field is invalid. + Slēptā lauka vērtība ir nederīga. + + + Please enter an integer. + Lūdzu, ievadiet veselu skaitli. + + + Please select a valid language. + Lūdzu, izvēlieties derīgu valodu. + + + Please select a valid locale. + Lūdzu, izvēlieties derīgu lokalizāciju. + + + Please enter a valid money amount. + Lūdzu, ievadiet derīgu naudas lielumu. + + + Please enter a number. + Lūdzu, ievadiet skaitli. + + + The password is invalid. + Parole ir nederīga. + + + Please enter a percentage value. + Lūdzu, ievadiet procentuālo lielumu. + + + The values do not match. + Vērtības nesakrīt. + + + Please enter a valid time. + Lūdzu, ievadiet derīgu laiku. + + + Please select a valid timezone. + Lūdzu, izvēlieties derīgu laika zonu. + + + Please enter a valid URL. + Lūdzu, ievadiet derīgu URL. + + + Please enter a valid search term. + Lūdzu, ievadiet derīgu meklēšanas nosacījumu. + + + Please provide a valid phone number. + Lūdzu, ievadiet derīgu tālruņa numuru. + + + The checkbox has an invalid value. + Izvēles rūtiņai ir nederīga vērtība. + + + Please enter a valid email address. + Lūdzu, ievadiet derīgu e-pasta adresi. + + + Please select a valid option. + Lūdzu, izvēlieties derīgu opciju. + + + Please select a valid range. + Lūdzu, izvēlieties derīgu diapazonu. + + + Please enter a valid week. + Lūdzu, ievadiet derīgu nedēļu. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.mk.xlf b/lib/symfony/form/Resources/translations/validators.mk.xlf new file mode 100644 index 0000000000..5f2af85eb5 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.mk.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Оваа форма не треба да содржи дополнителни полиња. + + + The uploaded file was too large. Please try to upload a smaller file. + Датотеката што се обидовте да ја подигнете е преголема. Ве молиме обидете се со помала датотека. + + + The CSRF token is invalid. Please try to resubmit the form. + Вашиот CSRF токен е невалиден. Ве молиме испратете ја формата одново. + + + This value is not a valid HTML5 color. + Оваа вредност не е валидна HTML5 боја. + + + Please enter a valid birthdate. + Ве молиме внесете валидна дата на раѓање. + + + The selected choice is invalid. + Избраната опција е невалидна. + + + The collection is invalid. + Колекцијата е невалидна. + + + Please select a valid color. + Ве молиме одберете валидна боја. + + + Please select a valid country. + Ве молиме одберете валидна земја. + + + Please select a valid currency. + Ве молиме одберете валидна валута. + + + Please choose a valid date interval. + Ве молиме одберете валиден интервал помеѓу два датума. + + + Please enter a valid date and time. + Ве молиме внесете валиден датум и време. + + + Please enter a valid date. + Ве молиме внесете валиден датум. + + + Please select a valid file. + Ве молиме одберете валидна датотека. + + + The hidden field is invalid. + Скриеното поле е невалидно. + + + Please enter an integer. + Ве молиме внесете цел број. + + + Please select a valid language. + Ве молиме одберете валиден јазик. + + + Please select a valid locale. + Ве молиме одберете валидна локализација. + + + Please enter a valid money amount. + Ве молиме внесете валидна сума на пари. + + + Please enter a number. + Ве молиме внесете број. + + + The password is invalid. + Лозинката е погрешна. + + + Please enter a percentage value. + Ве молиме внесете процентуална вредност. + + + The values do not match. + Вредностите не се совпаѓаат. + + + Please enter a valid time. + Ве молиме внесете валидно време. + + + Please select a valid timezone. + Ве молиме одберете валидна временска зона. + + + Please enter a valid URL. + Ве молиме внесете валиден униформен локатор на ресурси (URL). + + + Please enter a valid search term. + Ве молиме внесете валиден термин за пребарување. + + + Please provide a valid phone number. + Ве молиме внесете валиден телефонски број. + + + The checkbox has an invalid value. + Полето за штиклирање има неважечка вредност. + + + Please enter a valid email address. + Ве молиме внесете валидна адреса за е-пошта. + + + Please select a valid option. + Ве молиме одберете валидна опција. + + + Please select a valid range. + Ве молиме одберете важечки опсег. + + + Please enter a valid week. + Ве молиме внесете валидна недела. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.mn.xlf b/lib/symfony/form/Resources/translations/validators.mn.xlf new file mode 100644 index 0000000000..2e6d09bc6b --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.mn.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Форм нэмэлт талбар багтаах боломжгүй. + + + The uploaded file was too large. Please try to upload a smaller file. + Upload хийсэн файл хэтэрхий том байна. Бага хэмжээтэй файл оруулна уу. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF token буруу байна. Формоо дахин илгээнэ үү. + + + This value is not a valid HTML5 color. + Энэ утга зөв HTML5 өнгө биш байна. + + + Please enter a valid birthdate. + Зөв төрсөн он сар оруулна уу. + + + The selected choice is invalid. + Сонгосон утга буруу байна. + + + The collection is invalid. + Цуглуулга буруу байна. + + + Please select a valid color. + Үнэн зөв өнгө сонгоно уу. + + + Please select a valid country. + Үнэн зөв улс сонгоно уу. + + + Please select a valid currency. + Үнэн зөв мөнгөн тэмдэгт сонгоно уу. + + + Please choose a valid date interval. + Үнэн зөв цагын зай сонгоно уу. + + + Please enter a valid date and time. + Үнэн зөв он цаг оруулна уу. + + + Please enter a valid date. + Үнэн зөв он цаг өдөр оруулна уу. + + + Please select a valid file. + Үнэн зөв файл сонгоно уу. + + + The hidden field is invalid. + Нууц талбарын утга буруу байна. + + + Please enter an integer. + Бүхэл тоо оруулна уу. + + + Please select a valid language. + Үнэн зөв хэл сонгоно уу. + + + Please select a valid locale. + Үнэн зөв бүс сонгоно уу. + + + Please enter a valid money amount. + Үнэн зөв мөнгөний хэмжээ сонгоно уу. + + + Please enter a number. + Тоо оруулна уу. + + + The password is invalid. + Нууц үг буруу байна. + + + Please enter a percentage value. + Хувь утга оруулна уу. + + + The values do not match. + Утга хоорондоо таарахгүй байна. + + + Please enter a valid time. + Үнэн зөв цаг оруулна уу. + + + Please select a valid timezone. + Үнэн зөв цагын бүс оруулна уу. + + + Please enter a valid URL. + Үнэн зөв URL оруулна уу. + + + Please enter a valid search term. + Үнэн зөв хайх утга оруулна уу. + + + Please provide a valid phone number. + Үнэн зөв утасны дугаар оруулна уу. + + + The checkbox has an invalid value. + Сонгох хайрцаг буруу утгатай байна. + + + Please enter a valid email address. + Үнэн зөв и-мэйл хаяг оруулна уу. + + + Please select a valid option. + Үнэн зөв сонголт сонгоно уу. + + + Please select a valid range. + Үнэн зөв хязгаарын утга сонгоно уу. + + + Please enter a valid week. + Үнэн зөв долоо хоног сонгоно уу. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.my.xlf b/lib/symfony/form/Resources/translations/validators.my.xlf new file mode 100644 index 0000000000..9ecb9d368a --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.my.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + ဤ ဖောင်သည် field အပိုများ မပါ၀င်သင့်ပါ။ + + + The uploaded file was too large. Please try to upload a smaller file. + Upload တင်သောဖိုင်သည်အလွန်ကြီးလွန်းသည်။ ကျေးဇူးပြု၍ သေးငယ်သည့်ဖိုင်ကိုတင်ရန်ကြိုးစားပါ။ + + + The CSRF token is invalid. Please try to resubmit the form. + သင့်လျှော်သော် CSRF တိုကင် မဟုတ်ပါ။ ကျေးဇူးပြု၍ဖောင်ကိုပြန်တင်ပါ။ + + + This value is not a valid HTML5 color. + ဤတန်ဖိုးသည် သင့်လျှော်သော် HTML5 အရောင်မဟုတ်ပါ။ + + + Please enter a valid birthdate. + ကျေးဇူးပြု၍ မှန်ကန်သောမွေးနေ့ကိုထည့်ပါ။ + + + The selected choice is invalid. + သင့် ရွေးချယ်မှုသည်မမှန်ကန်ပါ။ + + + The collection is invalid. + ဤ collection သည်သင့်လျှော်သော် collection မဟုတ်ပါ။ + + + Please select a valid color. + ကျေးဇူးပြု၍ မှန်ကန်သောအရောင်ကိုရွေးပါ။ + + + Please select a valid country. + ကျေးဇူးပြု၍ မှန်ကန်သောနိုင်ငံကိုရွေးပါ။ + + + Please select a valid currency. + ကျေးဇူးပြု၍ မှန်ကန်သောငွေကြေးကိုရွေးပါ။ + + + Please choose a valid date interval. + ကျေးဇူးပြု၍ မှန်ကန်သောနေ ရက်စွဲကိုရွေးပါ။ + + + Please enter a valid date and time. + ကျေးဇူးပြု၍ မှန်ကန်သောနေ ရက်စွဲနှင့်အချိန် ကိုထည့်ပါ။ + + + Please enter a valid date. + ကျေးဇူးပြု၍ မှန်ကန်သောနေ ရက်စွဲကိုထည့်ပါ။ + + + Please select a valid file. + ကျေးဇူးပြု၍ မှန်ကန်သောနေ ဖိုင်ကိုရွေးချယ်ပါ။ + + + The hidden field is invalid. + မသင့် လျှော်သော် hidden field ဖြစ်နေသည်။ + + + Please enter an integer. + ကျေးဇူးပြု၍ Integer တန်ဖိုးသာထည့်ပါ။ + + + Please select a valid language. + ကျေးဇူးပြု၍ မှန်ကန်သော ဘာသာစကားကိုရွေးချယ်ပါ။ + + + Please select a valid locale. + ကျေးဇူးပြု၍ မှန်ကန်သော locale ကိုရွေးချယ်ပါ။ + + + Please enter a valid money amount. + ကျေးဇူးပြု၍ မှန်ကန်သော ပိုက်ဆံပမာဏ ကိုထည့်ပါ။ + + + Please enter a number. + ကျေးဇူးပြု၍ မှန်ကန်သော နံပါတ် ကိုရွေးချယ်ပါ။ + + + The password is invalid. + မှန်ကန်သောစကား၀ှက်မဟုတ်ပါ။ + + + Please enter a percentage value. + ကျေးဇူးပြု၍ ရာခိုင်နှုန်းတန်ဖိုးထည့်ပါ။ + + + The values do not match. + တန်ဖိုးများကိုက်ညီမှုမရှိပါ။ + + + Please enter a valid time. + ကျေးဇူးပြု၍ မှန်ကန်သောအချိန်ကိုထည့်ပါ။ + + + Please select a valid timezone. + ကျေးဇူးပြု၍ မှန်ကန်သောအချိန်ဇုန်ကိုရွေးပါ။ + + + Please enter a valid URL. + ကျေးဇူးပြု၍ သင့်လျှော်သော် URL ကိုရွေးပါ။ + + + Please enter a valid search term. + ကျေးဇူးပြု၍ သင့် လျှော်သော်ရှာဖွေမှု term များထည့်ပါ။ + + + Please provide a valid phone number. + ကျေးဇူးပြု၍ သင့် လျှော်သော်ရှာဖွေမှု ဖုန်းနံပါတ်ထည့်ပါ။ + + + The checkbox has an invalid value. + Checkbox တန်ဖိုးသည် မှန်ကန်မှုမရှိပါ။ + + + Please enter a valid email address. + ကျေးဇူးပြု၍ မှန်ကန်သော် email လိပ်စာထည့်ပါ။ + + + Please select a valid option. + ကျေးဇူးပြု၍ မှန်ကန်သော် ရွေးချယ်မှု ကိုရွေးပါ။ + + + Please select a valid range. + ကျေးဇူးပြု၍ မှန်ကန်သော အပိုင်းအခြား ကိုရွေးပါ။ + + + Please enter a valid week. + ကျေးဇူးပြု၍ မှန်ကန်သောရက်သတ္တပတ်ကိုထည့်ပါ။ + + + + diff --git a/lib/symfony/form/Resources/translations/validators.nb.xlf b/lib/symfony/form/Resources/translations/validators.nb.xlf new file mode 100644 index 0000000000..193306b719 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.nb.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Feltgruppen må ikke inneholde ekstra felter. + + + The uploaded file was too large. Please try to upload a smaller file. + Den opplastede filen var for stor. Vennligst last opp en mindre fil. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-tokenen er ugyldig. Vennligst prøv å sende inn skjemaet på nytt. + + + This value is not a valid HTML5 color. + Denne verdien er ikke en gyldig HTML5-farge. + + + Please enter a valid birthdate. + Vennligst oppgi gyldig fødselsdato. + + + The selected choice is invalid. + Det valgte valget er ugyldig. + + + The collection is invalid. + Samlingen er ugyldig. + + + Please select a valid color. + Velg en gyldig farge. + + + Please select a valid country. + Vennligst velg et gyldig land. + + + Please select a valid currency. + Vennligst velg en gyldig valuta. + + + Please choose a valid date interval. + Vennligst velg et gyldig datointervall. + + + Please enter a valid date and time. + Vennligst angi en gyldig dato og tid. + + + Please enter a valid date. + Vennligst oppgi en gyldig dato. + + + Please select a valid file. + Vennligst velg en gyldig fil. + + + The hidden field is invalid. + Det skjulte feltet er ugyldig. + + + Please enter an integer. + Vennligst skriv inn et heltall. + + + Please select a valid language. + Vennligst velg et gyldig språk. + + + Please select a valid locale. + Vennligst velg et gyldig sted. + + + Please enter a valid money amount. + Vennligst angi et gyldig pengebeløp. + + + Please enter a number. + Vennligst skriv inn et nummer. + + + The password is invalid. + Passordet er ugyldig. + + + Please enter a percentage value. + Vennligst angi en prosentverdi. + + + The values do not match. + Verdiene stemmer ikke overens. + + + Please enter a valid time. + Vennligst angi et gyldig tidspunkt. + + + Please select a valid timezone. + Vennligst velg en gyldig tidssone. + + + Please enter a valid URL. + Vennligst skriv inn en gyldig URL. + + + Please enter a valid search term. + Vennligst angi et gyldig søketerm. + + + Please provide a valid phone number. + Vennligst oppgi et gyldig telefonnummer. + + + The checkbox has an invalid value. + Avkrysningsboksen har en ugyldig verdi. + + + Please enter a valid email address. + Vennligst skriv inn en gyldig e-post adresse. + + + Please select a valid option. + Vennligst velg et gyldig alternativ. + + + Please select a valid range. + Vennligst velg et gyldig område. + + + Please enter a valid week. + Vennligst skriv inn en gyldig uke. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.nl.xlf b/lib/symfony/form/Resources/translations/validators.nl.xlf new file mode 100644 index 0000000000..6330ecf8a3 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.nl.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Dit formulier mag geen extra velden bevatten. + + + The uploaded file was too large. Please try to upload a smaller file. + Het geüploade bestand is te groot. Probeer een kleiner bestand te uploaden. + + + The CSRF token is invalid. Please try to resubmit the form. + De CSRF-token is ongeldig. Probeer het formulier opnieuw te versturen. + + + This value is not a valid HTML5 color. + Dit is geen geldige HTML5 kleur. + + + Please enter a valid birthdate. + Vul een geldige geboortedatum in. + + + The selected choice is invalid. + Deze keuze is ongeldig. + + + The collection is invalid. + Deze collectie is ongeldig. + + + Please select a valid color. + Kies een geldige kleur. + + + Please select a valid country. + Kies een geldige landnaam. + + + Please select a valid currency. + Kies een geldige valuta. + + + Please choose a valid date interval. + Kies een geldig tijdinterval. + + + Please enter a valid date and time. + Vul een geldige datum en tijd in. + + + Please enter a valid date. + Vul een geldige datum in. + + + Please select a valid file. + Kies een geldig bestand. + + + The hidden field is invalid. + Het verborgen veld is incorrect. + + + Please enter an integer. + Vul een geldig getal in. + + + Please select a valid language. + Kies een geldige taal. + + + Please select a valid locale. + Kies een geldige locale. + + + Please enter a valid money amount. + Vul een geldig bedrag in. + + + Please enter a number. + Vul een geldig getal in. + + + The password is invalid. + Het wachtwoord is incorrect. + + + Please enter a percentage value. + Vul een geldig percentage in. + + + The values do not match. + De waardes komen niet overeen. + + + Please enter a valid time. + Vul een geldige tijd in. + + + Please select a valid timezone. + Vul een geldige tijdzone in. + + + Please enter a valid URL. + Vul een geldige URL in. + + + Please enter a valid search term. + Vul een geldige zoekterm in. + + + Please provide a valid phone number. + Vul een geldig telefoonnummer in. + + + The checkbox has an invalid value. + De checkbox heeft een incorrecte waarde. + + + Please enter a valid email address. + Vul een geldig e-mailadres in. + + + Please select a valid option. + Kies een geldige optie. + + + Please select a valid range. + Kies een geldig bereik. + + + Please enter a valid week. + Vul een geldige week in. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.nn.xlf b/lib/symfony/form/Resources/translations/validators.nn.xlf new file mode 100644 index 0000000000..0722b45687 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.nn.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Feltgruppa kan ikkje innehalde ekstra felt. + + + The uploaded file was too large. Please try to upload a smaller file. + Fila du lasta opp var for stor. Last opp ei mindre fil. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-teiknet er ugyldig. Ver venleg og prøv å sende inn skjemaet på nytt. + + + This value is not a valid HTML5 color. + Verdien er ikkje ein gyldig HTML5-farge. + + + Please enter a valid birthdate. + Gje opp ein gyldig fødselsdato. + + + The selected choice is invalid. + Valget du gjorde er ikkje gyldig. + + + The collection is invalid. + Samlinga er ikkje gyldig. + + + Please select a valid color. + Gje opp ein gyldig farge. + + + Please select a valid country. + Gje opp eit gyldig land. + + + Please select a valid currency. + Gje opp ein gyldig valuta. + + + Please choose a valid date interval. + Gje opp eit gyldig datointervall. + + + Please enter a valid date and time. + Gje opp ein gyldig dato og tid. + + + Please enter a valid date. + Gje opp ein gyldig dato. + + + Please select a valid file. + Velg ei gyldig fil. + + + The hidden field is invalid. + Det skjulte feltet er ikkje gyldig. + + + Please enter an integer. + Gje opp eit heiltal. + + + Please select a valid language. + Gje opp eit gyldig språk. + + + Please select a valid locale. + Gje opp eit gyldig locale. + + + Please enter a valid money amount. + Gje opp ein gyldig sum pengar. + + + Please enter a number. + Gje opp eit nummer. + + + The password is invalid. + Passordet er ikkje gyldig. + + + Please enter a percentage value. + Gje opp ein prosentverdi. + + + The values do not match. + Verdiane er ikkje eins. + + + Please enter a valid time. + Gje opp ei gyldig tid. + + + Please select a valid timezone. + Gje opp ei gyldig tidssone. + + + Please enter a valid URL. + Gje opp ein gyldig URL. + + + Please enter a valid search term. + Gje opp gyldige søkjeord. + + + Please provide a valid phone number. + Gje opp eit gyldig telefonnummer. + + + The checkbox has an invalid value. + Sjekkboksen har ein ugyldig verdi. + + + Please enter a valid email address. + Gje opp ei gyldig e-postadresse. + + + Please select a valid option. + Velg eit gyldig vilkår. + + + Please select a valid range. + Velg eit gyldig spenn. + + + Please enter a valid week. + Gje opp ei gyldig veke. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.no.xlf b/lib/symfony/form/Resources/translations/validators.no.xlf new file mode 100644 index 0000000000..193306b719 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.no.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Feltgruppen må ikke inneholde ekstra felter. + + + The uploaded file was too large. Please try to upload a smaller file. + Den opplastede filen var for stor. Vennligst last opp en mindre fil. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-tokenen er ugyldig. Vennligst prøv å sende inn skjemaet på nytt. + + + This value is not a valid HTML5 color. + Denne verdien er ikke en gyldig HTML5-farge. + + + Please enter a valid birthdate. + Vennligst oppgi gyldig fødselsdato. + + + The selected choice is invalid. + Det valgte valget er ugyldig. + + + The collection is invalid. + Samlingen er ugyldig. + + + Please select a valid color. + Velg en gyldig farge. + + + Please select a valid country. + Vennligst velg et gyldig land. + + + Please select a valid currency. + Vennligst velg en gyldig valuta. + + + Please choose a valid date interval. + Vennligst velg et gyldig datointervall. + + + Please enter a valid date and time. + Vennligst angi en gyldig dato og tid. + + + Please enter a valid date. + Vennligst oppgi en gyldig dato. + + + Please select a valid file. + Vennligst velg en gyldig fil. + + + The hidden field is invalid. + Det skjulte feltet er ugyldig. + + + Please enter an integer. + Vennligst skriv inn et heltall. + + + Please select a valid language. + Vennligst velg et gyldig språk. + + + Please select a valid locale. + Vennligst velg et gyldig sted. + + + Please enter a valid money amount. + Vennligst angi et gyldig pengebeløp. + + + Please enter a number. + Vennligst skriv inn et nummer. + + + The password is invalid. + Passordet er ugyldig. + + + Please enter a percentage value. + Vennligst angi en prosentverdi. + + + The values do not match. + Verdiene stemmer ikke overens. + + + Please enter a valid time. + Vennligst angi et gyldig tidspunkt. + + + Please select a valid timezone. + Vennligst velg en gyldig tidssone. + + + Please enter a valid URL. + Vennligst skriv inn en gyldig URL. + + + Please enter a valid search term. + Vennligst angi et gyldig søketerm. + + + Please provide a valid phone number. + Vennligst oppgi et gyldig telefonnummer. + + + The checkbox has an invalid value. + Avkrysningsboksen har en ugyldig verdi. + + + Please enter a valid email address. + Vennligst skriv inn en gyldig e-post adresse. + + + Please select a valid option. + Vennligst velg et gyldig alternativ. + + + Please select a valid range. + Vennligst velg et gyldig område. + + + Please enter a valid week. + Vennligst skriv inn en gyldig uke. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.pl.xlf b/lib/symfony/form/Resources/translations/validators.pl.xlf new file mode 100644 index 0000000000..767f05d29f --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.pl.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ten formularz nie powinien zawierać dodatkowych pól. + + + The uploaded file was too large. Please try to upload a smaller file. + Wgrany plik był za duży. Proszę spróbować wgrać mniejszy plik. + + + The CSRF token is invalid. Please try to resubmit the form. + Token CSRF jest nieprawidłowy. Proszę spróbować wysłać formularz ponownie. + + + This value is not a valid HTML5 color. + Ta wartość nie jest prawidłowym kolorem HTML5. + + + Please enter a valid birthdate. + Proszę wprowadzić prawidłową datę urodzenia. + + + The selected choice is invalid. + Wybrana wartość jest nieprawidłowa. + + + The collection is invalid. + Zbiór jest nieprawidłowy. + + + Please select a valid color. + Proszę wybrać prawidłowy kolor. + + + Please select a valid country. + Proszę wybrać prawidłowy kraj. + + + Please select a valid currency. + Proszę wybrać prawidłową walutę. + + + Please choose a valid date interval. + Proszę wybrać prawidłowy przedział czasowy. + + + Please enter a valid date and time. + Proszę wprowadzić prawidłową datę i czas. + + + Please enter a valid date. + Proszę wprowadzić prawidłową datę. + + + Please select a valid file. + Proszę wybrać prawidłowy plik. + + + The hidden field is invalid. + Ukryte pole jest nieprawidłowe. + + + Please enter an integer. + Proszę wprowadzić liczbę całkowitą. + + + Please select a valid language. + Proszę wybrać prawidłowy język. + + + Please select a valid locale. + Proszę wybrać prawidłową lokalizację. + + + Please enter a valid money amount. + Proszę wybrać prawidłową ilość pieniędzy. + + + Please enter a number. + Proszę wprowadzić liczbę. + + + The password is invalid. + Hasło jest nieprawidłowe. + + + Please enter a percentage value. + Proszę wprowadzić wartość procentową. + + + The values do not match. + Wartości się nie zgadzają. + + + Please enter a valid time. + Proszę wprowadzić prawidłowy czas. + + + Please select a valid timezone. + Proszę wybrać prawidłową strefę czasową. + + + Please enter a valid URL. + Proszę wprowadzić prawidłowy adres URL. + + + Please enter a valid search term. + Proszę wprowadzić prawidłowy termin wyszukiwania. + + + Please provide a valid phone number. + Proszę wprowadzić prawidłowy numer telefonu. + + + The checkbox has an invalid value. + Pole wyboru posiada nieprawidłową wartość. + + + Please enter a valid email address. + Proszę wprowadzić prawidłowy adres email. + + + Please select a valid option. + Proszę wybrać prawidłową opcję. + + + Please select a valid range. + Proszę wybrać prawidłowy zakres. + + + Please enter a valid week. + Proszę wybrać prawidłowy tydzień. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.pt.xlf b/lib/symfony/form/Resources/translations/validators.pt.xlf new file mode 100644 index 0000000000..673e79f420 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.pt.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Este formulário não deveria possuir mais campos. + + + The uploaded file was too large. Please try to upload a smaller file. + O ficheiro enviado é muito grande. Por favor, tente enviar um ficheiro menor. + + + The CSRF token is invalid. Please try to resubmit the form. + O token CSRF está inválido. Por favor, tente enviar o formulário novamente. + + + This value is not a valid HTML5 color. + Este valor não é uma cor HTML5 válida. + + + Please enter a valid birthdate. + Por favor, informe uma data de nascimento válida. + + + The selected choice is invalid. + A escolha selecionada é inválida. + + + The collection is invalid. + A coleção é inválida. + + + Please select a valid color. + Por favor, selecione uma cor válida. + + + Please select a valid country. + Por favor, selecione um país válido. + + + Please select a valid currency. + Por favor, selecione uma moeda válida. + + + Please choose a valid date interval. + Por favor, escolha um intervalo de datas válido. + + + Please enter a valid date and time. + Por favor, informe uma data e horário válidos. + + + Please enter a valid date. + Por favor, informe uma data válida. + + + Please select a valid file. + Por favor, selecione um ficheiro válido. + + + The hidden field is invalid. + O campo oculto é inválido. + + + Please enter an integer. + Por favor, informe um inteiro. + + + Please select a valid language. + Por favor selecione um idioma válido. + + + Please select a valid locale. + Por favor, selecione um locale válido. + + + Please enter a valid money amount. + Por favor, informe um valor monetário válido. + + + Please enter a number. + Por favor, informe um número. + + + The password is invalid. + A palavra-passe é inválida. + + + Please enter a percentage value. + Por favor, informe um valor percentual. + + + The values do not match. + Os valores não correspondem. + + + Please enter a valid time. + Por favor, informe uma hora válida. + + + Please select a valid timezone. + Por favor, selecione um fuso horário válido. + + + Please enter a valid URL. + Por favor, informe uma URL válida. + + + Please enter a valid search term. + Por favor, informe um termo de busca válido. + + + Please provide a valid phone number. + Por favor, infome um número de telefone válido. + + + The checkbox has an invalid value. + O checkbox possui um valor inválido. + + + Please enter a valid email address. + Por favor, informe um endereço de email válido. + + + Please select a valid option. + Por favor, selecione uma opção válida. + + + Please select a valid range. + Por favor, selecione um intervalo válido. + + + Please enter a valid week. + Por favor, selecione uma semana válida. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.pt_BR.xlf b/lib/symfony/form/Resources/translations/validators.pt_BR.xlf new file mode 100644 index 0000000000..c386ab3049 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.pt_BR.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Este formulário não deve conter campos adicionais. + + + The uploaded file was too large. Please try to upload a smaller file. + O arquivo enviado é muito grande. Por favor, tente enviar um arquivo menor. + + + The CSRF token is invalid. Please try to resubmit the form. + O token CSRF é inválido. Por favor, tente reenviar o formulário. + + + This value is not a valid HTML5 color. + Este valor não é uma cor HTML5 válida. + + + Please enter a valid birthdate. + Por favor, informe uma data de nascimento válida. + + + The selected choice is invalid. + A escolha selecionada é inválida. + + + The collection is invalid. + A coleção é inválida. + + + Please select a valid color. + Por favor, selecione uma cor válida. + + + Please select a valid country. + Por favor, selecione um país válido. + + + Please select a valid currency. + Por favor, selecione uma moeda válida. + + + Please choose a valid date interval. + Por favor, escolha um intervalo de datas válido. + + + Please enter a valid date and time. + Por favor, informe uma data e horário válidos. + + + Please enter a valid date. + Por favor, informe uma data válida. + + + Please select a valid file. + Por favor, selecione um arquivo válido. + + + The hidden field is invalid. + O campo oculto é inválido. + + + Please enter an integer. + Por favor, informe um número inteiro. + + + Please select a valid language. + Por favor, selecione um idioma válido. + + + Please select a valid locale. + Por favor, selecione uma configuração de local válida. + + + Please enter a valid money amount. + Por favor, informe um valor monetário válido. + + + Please enter a number. + Por favor, informe um número. + + + The password is invalid. + A senha é inválida. + + + Please enter a percentage value. + Por favor, informe um valor percentual. + + + The values do not match. + Os valores não conferem. + + + Please enter a valid time. + Por favor, informe um horário válido. + + + Please select a valid timezone. + Por favor, selecione um fuso horário válido. + + + Please enter a valid URL. + Por favor, informe uma URL válida. + + + Please enter a valid search term. + Por favor, informe um termo de busca válido. + + + Please provide a valid phone number. + Por favor, informe um telefone válido. + + + The checkbox has an invalid value. + A caixa de seleção possui um valor inválido. + + + Please enter a valid email address. + Por favor, informe um endereço de e-mail válido. + + + Please select a valid option. + Por favor, selecione uma opção válida. + + + Please select a valid range. + Por favor, selecione um intervalo válido. + + + Please enter a valid week. + Por favor, informe uma semana válida. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.ro.xlf b/lib/symfony/form/Resources/translations/validators.ro.xlf new file mode 100644 index 0000000000..63b4c551ff --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.ro.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Acest formular nu ar trebui să conțină câmpuri suplimentare. + + + The uploaded file was too large. Please try to upload a smaller file. + Fișierul încărcat a fost prea mare. Vă rugăm sa încărcați un fișier mai mic. + + + The CSRF token is invalid. Please try to resubmit the form. + Token-ul CSRF este invalid. Vă rugăm să retrimiteți formularul. + + + This value is not a valid HTML5 color. + Această valoare nu este un cod de culoare HTML5 valid. + + + Please enter a valid birthdate. + Vă rugăm să introduceți o dată de naștere validă. + + + The selected choice is invalid. + Valoarea selectată este invalidă. + + + The collection is invalid. + Colecția nu este validă. + + + Please select a valid color. + Vă rugăm să selectați o culoare validă. + + + Please select a valid country. + Vă rugăm să selectați o țară validă. + + + Please select a valid currency. + Vă rugăm să selectați o monedă validă. + + + Please choose a valid date interval. + Vă rugăm să selectați un interval de zile valid. + + + Please enter a valid date and time. + Vă rugăm să introduceți o dată și o oră validă. + + + Please enter a valid date. + Vă rugăm să introduceți o dată validă. + + + Please select a valid file. + Vă rugăm să selectați un fișier valid. + + + The hidden field is invalid. + Câmpul ascuns este invalid. + + + Please enter an integer. + Vă rugăm să introduceți un număr întreg. + + + Please select a valid language. + Vă rugăm să selectați o limbă validă. + + + Please select a valid locale. + Vă rugăm să selectați o setare locală validă. + + + Please enter a valid money amount. + Vă rugăm să introduceți o valoare monetară corectă. + + + Please enter a number. + Vă rugăm să introduceți un număr. + + + The password is invalid. + Parola nu este validă. + + + Please enter a percentage value. + Vă rugăm să introduceți o valoare procentuală. + + + The values do not match. + Valorile nu coincid. + + + Please enter a valid time. + Vă rugăm să introduceți o oră validă. + + + Please select a valid timezone. + Vă rugăm să selectați un fus orar valid. + + + Please enter a valid URL. + Vă rugăm să introduceți un URL valid. + + + Please enter a valid search term. + Vă rugăm să introduceți un termen de căutare valid. + + + Please provide a valid phone number. + Vă rugăm să introduceți un număr de telefon valid. + + + The checkbox has an invalid value. + Bifa nu are o valoare validă. + + + Please enter a valid email address. + Vă rugăm să introduceți o adresă de email validă. + + + Please select a valid option. + Vă rugăm să selectați o opțiune validă. + + + Please select a valid range. + Vă rugăm să selectați un interval valid. + + + Please enter a valid week. + Vă rugăm să introduceți o săptămână validă. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.ru.xlf b/lib/symfony/form/Resources/translations/validators.ru.xlf new file mode 100644 index 0000000000..26535d26d3 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.ru.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Эта форма не должна содержать дополнительных полей. + + + The uploaded file was too large. Please try to upload a smaller file. + Загруженный файл слишком большой. Пожалуйста, попробуйте загрузить файл меньшего размера. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF значение недопустимо. Пожалуйста, попробуйте повторить отправку формы. + + + This value is not a valid HTML5 color. + Значение не является допустимым HTML5 цветом. + + + Please enter a valid birthdate. + Пожалуйста, введите действительную дату рождения. + + + The selected choice is invalid. + Выбранный вариант недопустим. + + + The collection is invalid. + Коллекция недопустима. + + + Please select a valid color. + Пожалуйста, выберите допустимый цвет. + + + Please select a valid country. + Пожалуйста, выберите действительную страну. + + + Please select a valid currency. + Пожалуйста, выберите действительную валюту. + + + Please choose a valid date interval. + Пожалуйста, выберите действительный период. + + + Please enter a valid date and time. + Пожалуйста, введите действительные дату и время. + + + Please enter a valid date. + Пожалуйста, введите действительную дату. + + + Please select a valid file. + Пожалуйста, выберите допустимый файл. + + + The hidden field is invalid. + Значение скрытого поля недопустимо. + + + Please enter an integer. + Пожалуйста, введите целое число. + + + Please select a valid language. + Пожалуйста, выберите допустимый язык. + + + Please select a valid locale. + Пожалуйста, выберите допустимую локаль. + + + Please enter a valid money amount. + Пожалуйста, введите допустимое количество денег. + + + Please enter a number. + Пожалуйста, введите номер. + + + The password is invalid. + Пароль недействителен. + + + Please enter a percentage value. + Пожалуйста, введите процентное значение. + + + The values do not match. + Значения не совпадают. + + + Please enter a valid time. + Пожалуйста, введите действительное время. + + + Please select a valid timezone. + Пожалуйста, выберите действительный часовой пояс. + + + Please enter a valid URL. + Пожалуйста, введите действительный URL. + + + Please enter a valid search term. + Пожалуйста, введите действительный поисковый запрос. + + + Please provide a valid phone number. + Пожалуйста, введите действительный номер телефона. + + + The checkbox has an invalid value. + Флажок имеет недопустимое значение. + + + Please enter a valid email address. + Пожалуйста, введите допустимый email адрес. + + + Please select a valid option. + Пожалуйста, выберите допустимый вариант. + + + Please select a valid range. + Пожалуйста, выберите допустимый диапазон. + + + Please enter a valid week. + Пожалуйста, введите действительную неделю. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.sk.xlf b/lib/symfony/form/Resources/translations/validators.sk.xlf new file mode 100644 index 0000000000..72ecd13e18 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.sk.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Polia by nemali obsahovať ďalšie prvky. + + + The uploaded file was too large. Please try to upload a smaller file. + Odoslaný súbor je príliš veľký. Prosím odošlite súbor s menšou veľkosťou. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF token je neplatný. Prosím skúste znovu odoslať formulár. + + + This value is not a valid HTML5 color. + Táto hodnota nie je platná HTML5 farba. + + + Please enter a valid birthdate. + Prosím zadajte platný dátum narodenia. + + + The selected choice is invalid. + Vybraná možnosť je neplatná. + + + The collection is invalid. + Kolekcia je neplatná. + + + Please select a valid color. + Prosím vyberte platnú farbu. + + + Please select a valid country. + Prosím vyberte platnú krajinu. + + + Please select a valid currency. + Prosím vyberte platnú menu. + + + Please choose a valid date interval. + Prosím vyberte platný rozsah dát. + + + Please enter a valid date and time. + Prosím zadajte platný dátum a čas. + + + Please enter a valid date. + Prosím zadajte platný dátum. + + + Please select a valid file. + Prosím vyberte platný súbor. + + + The hidden field is invalid. + Skryté pole je neplatné. + + + Please enter an integer. + Prosím zadajte celé číslo. + + + Please select a valid language. + Prosím vyberte platný jazyk. + + + Please select a valid locale. + Prosím vyberte platné miestne nastavenia. + + + Please enter a valid money amount. + Prosím zadajte platnú čiastku. + + + Please enter a number. + Prosím zadajte číslo. + + + The password is invalid. + Heslo je neprávne. + + + Please enter a percentage value. + Prosím zadajte percentuálnu hodnotu. + + + The values do not match. + Hodnoty nie sú zhodné. + + + Please enter a valid time. + Prosím zadajte platný čas. + + + Please select a valid timezone. + Prosím vyberte platné časové pásmo. + + + Please enter a valid URL. + Prosím zadajte platnú URL. + + + Please enter a valid search term. + Prosím zadajte platný vyhľadávací výraz. + + + Please provide a valid phone number. + Prosím zadajte platné telefónne číslo. + + + The checkbox has an invalid value. + Zaškrtávacie políčko má neplatnú hodnotu. + + + Please enter a valid email address. + Prosím zadajte platnú emailovú adresu. + + + Please select a valid option. + Prosím vyberte platnú možnosť. + + + Please select a valid range. + Prosím vyberte platný rozsah. + + + Please enter a valid week. + Prosím zadajte platný týždeň. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.sl.xlf b/lib/symfony/form/Resources/translations/validators.sl.xlf new file mode 100644 index 0000000000..c19949d713 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.sl.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ta obrazec ne sme vsebovati dodatnih polj. + + + The uploaded file was too large. Please try to upload a smaller file. + Naložena datoteka je prevelika. Prosimo, poizkusite naložiti manjšo. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF vrednost je napačna. Prosimo, ponovno pošljite obrazec. + + + This value is not a valid HTML5 color. + Ta vrednost ni veljavna barva HTML5. + + + Please enter a valid birthdate. + Prosimo, vnesite veljaven rojstni datum. + + + The selected choice is invalid. + Izbira ni veljavna. + + + The collection is invalid. + Zbirka ni veljavna. + + + Please select a valid color. + Prosimo, izberite veljavno barvo. + + + Please select a valid country. + Prosimo, izberite veljavno državo. + + + Please select a valid currency. + Prosimo, izberite veljavno valuto. + + + Please choose a valid date interval. + Prosimo, izberite veljaven datumski interval. + + + Please enter a valid date and time. + Prosimo, vnesite veljaven datum in čas. + + + Please enter a valid date. + Prosimo, izberite veljaven datum. + + + Please select a valid file. + Prosimo, izberite veljavno datoteko. + + + The hidden field is invalid. + Skrito polje ni veljavno. + + + Please enter an integer. + Prosimo, vnesite celo število. + + + Please select a valid language. + Prosimo, izberite veljaven jezik. + + + Please select a valid locale. + Prosimo, izberite veljavne področne nastavitve. + + + Please enter a valid money amount. + Prosimo, vnesite veljaven denarni znesek. + + + Please enter a number. + Prosimo, vnesite številko. + + + The password is invalid. + Geslo ni veljavno. + + + Please enter a percentage value. + Prosimo, vnesite odstotno vrednost. + + + The values do not match. + Vrednosti se ne ujemajo. + + + Please enter a valid time. + Prosimo, vnesite veljaven čas. + + + Please select a valid timezone. + Prosimo, izberite veljaven časovni pas. + + + Please enter a valid URL. + Prosimo, vnesite veljaven URL. + + + Please enter a valid search term. + Prosimo, vnesite veljaven iskalni izraz. + + + Please provide a valid phone number. + Prosimo, podajte veljavno telefonsko številko. + + + The checkbox has an invalid value. + Potrditveno polje vsebuje neveljavno vrednost. + + + Please enter a valid email address. + Prosimo, vnesite veljaven e-poštni naslov. + + + Please select a valid option. + Prosimo, izberite veljavno možnost. + + + Please select a valid range. + Prosimo, izberite veljaven obseg. + + + Please enter a valid week. + Prosimo, vnesite veljaven teden. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.sq.xlf b/lib/symfony/form/Resources/translations/validators.sq.xlf new file mode 100644 index 0000000000..0feb137f85 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.sq.xlf @@ -0,0 +1,148 @@ + + + +
+ + Për fjalët e huaja, të cilat nuk kanë përkthim të drejtpërdrejtë, ju lutemi të ndiqni rregullat e mëposhtme: + a) në rast se emri është akronim i përdorur gjerësisht si i përveçëm, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Gjinia gjykohet sipas rastit. Shembull: JSON-i (mashkullore) + b) në rast se emri është akronim i papërdorur gjerësisht si i përveçëm, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Gjinia është femërore. Shembull: URL-ja (femërore) + c) në rast se emri duhet lakuar për shkak të rasës në fjali, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Shembull: host-i, prej host-it + d) në rast se emri nuk duhet lakuar për shkak të trajtës në fjali, atëherë, emri rrethohet me thonjëzat “”. Shembull: “locale” + +
+ + + This form should not contain extra fields. + Ky formular nuk duhet të përmbajë fusha shtesë. + + + The uploaded file was too large. Please try to upload a smaller file. + Skeda e ngarkuar ishte shumë e madhe. Ju lutemi provoni të ngarkoni një skedë më të vogël. + + + The CSRF token is invalid. Please try to resubmit the form. + Vlera CSRF është e pavlefshme. Ju lutemi provoni të ridërgoni formularin. + + + This value is not a valid HTML5 color. + Kjo vlerë nuk është një ngjyrë e vlefshme HTML5. + + + Please enter a valid birthdate. + Ju lutemi shkruani një datëlindje të vlefshme. + + + The selected choice is invalid. + Alternativa e zgjedhur është e pavlefshme. + + + The collection is invalid. + Koleksioni është i pavlefshëm. + + + Please select a valid color. + Ju lutemi zgjidhni një ngjyrë të vlefshme. + + + Please select a valid country. + Ju lutemi zgjidhni një shtet të vlefshëm. + + + Please select a valid currency. + Ju lutemi zgjidhni një valutë të vlefshme. + + + Please choose a valid date interval. + Ju lutemi zgjidhni një interval të vlefshëm. + + + Please enter a valid date and time. + Ju lutemi shkruani një datë dhe orë të vlefshme. + + + Please enter a valid date. + Ju lutemi shkruani një datë të vlefshme. + + + Please select a valid file. + Ju lutemi zgjidhni një skedë të vlefshme. + + + The hidden field is invalid. + Fusha e fshehur është e pavlefshme. + + + Please enter an integer. + Ju lutemi shkruani një numër të plotë. + + + Please select a valid language. + Ju lutemi zgjidhni një gjuhë të vlefshme. + + + Please select a valid locale. + Ju lutemi zgjidhni një “locale” të vlefshme. + + + Please enter a valid money amount. + Ju lutemi shkruani një shumë të vlefshme parash. + + + Please enter a number. + Ju lutemi shkruani një numër. + + + The password is invalid. + Fjalëkalimi është i pavlefshëm. + + + Please enter a percentage value. + Ju lutemi shkruani një vlerë përqindjeje. + + + The values do not match. + Vlerat nuk përputhen. + + + Please enter a valid time. + Ju lutemi shkruani një orë të vlefshme. + + + Please select a valid timezone. + Ju lutemi zgjidhni një zonë kohore të vlefshme. + + + Please enter a valid URL. + Ju lutemi shkruani një URL të vlefshme. + + + Please enter a valid search term. + Ju lutemi shkruani një term të vlefshëm kërkimi. + + + Please provide a valid phone number. + Ju lutemi jepni një numër telefoni të vlefshëm. + + + The checkbox has an invalid value. + Kutia e zgjedhjes ka një vlerë të pavlefshme. + + + Please enter a valid email address. + Ju lutemi shkruani një adresë të vlefshme email-i. + + + Please select a valid option. + Ju lutemi zgjidhni një alternativë të vlefshme. + + + Please select a valid range. + Ju lutemi zgjidhni një seri të vlefshme. + + + Please enter a valid week. + Ju lutemi shkruani një javë të vlefshme. + + +
+
diff --git a/lib/symfony/form/Resources/translations/validators.sr_Cyrl.xlf b/lib/symfony/form/Resources/translations/validators.sr_Cyrl.xlf new file mode 100644 index 0000000000..4b3e5b9b8e --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.sr_Cyrl.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Овај формулар не треба да садржи додатна поља. + + + The uploaded file was too large. Please try to upload a smaller file. + Отпремљена датотека је била превелика. Молим покушајте отпремање мање датотеке. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF вредност није исправна. Покушајте поново. + + + This value is not a valid HTML5 color. + Ова вредност није исправна HTML5 боја. + + + Please enter a valid birthdate. + Молим упишите исправан датум рођења. + + + The selected choice is invalid. + Одабрани избор није исправан. + + + The collection is invalid. + Ова колекција није исправна. + + + Please select a valid color. + Молим изаберите исправну боју. + + + Please select a valid country. + Молим изаберите исправну државу. + + + Please select a valid currency. + Молим изаберите исправну валуту. + + + Please choose a valid date interval. + Молим изаберите исправан датумски интервал. + + + Please enter a valid date and time. + Молим упишите исправан датум и време. + + + Please enter a valid date. + Молим упишите исправан датум. + + + Please select a valid file. + Молим изаберите исправну датотеку. + + + The hidden field is invalid. + Скривено поље није исправно. + + + Please enter an integer. + Молим упишите цео број (integer). + + + Please select a valid language. + Молим изаберите исправан језик. + + + Please select a valid locale. + Молим изаберите исправну локализацију. + + + Please enter a valid money amount. + Молим упишите исправну количину новца. + + + Please enter a number. + Молим упишите број. + + + The password is invalid. + Ова лозинка није исправна. + + + Please enter a percentage value. + Молим упишите процентуалну вредност. + + + The values do not match. + Дате вредности се не поклапају. + + + Please enter a valid time. + Молим упишите исправно време. + + + Please select a valid timezone. + Молим изаберите исправну временску зону. + + + Please enter a valid URL. + Молим упишите исправан URL. + + + Please enter a valid search term. + Молим упишите исправан термин за претрагу. + + + Please provide a valid phone number. + Молим наведите исправан број телефона. + + + The checkbox has an invalid value. + Поље за потврду садржи неисправну вредност. + + + Please enter a valid email address. + Молим упишите исправну email адресу. + + + Please select a valid option. + Молим изаберите исправну опцију. + + + Please select a valid range. + Молим изаберите исправан опсег. + + + Please enter a valid week. + Молим упишите исправну седмицу. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.sr_Latn.xlf b/lib/symfony/form/Resources/translations/validators.sr_Latn.xlf new file mode 100644 index 0000000000..6f64f5634d --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.sr_Latn.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ovaj formular ne treba da sadrži dodatna polja. + + + The uploaded file was too large. Please try to upload a smaller file. + Otpremljena datoteka je bila prevelika. Molim pokušajte otpremanje manje datoteke. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF vrednost nije ispravna. Pokušajte ponovo. + + + This value is not a valid HTML5 color. + Ova vrednost nije ispravna HTML5 boja. + + + Please enter a valid birthdate. + Molim upišite ispravan datum rođenja. + + + The selected choice is invalid. + Odabrani izbor nije ispravan. + + + The collection is invalid. + Ova kolekcija nije ispravna. + + + Please select a valid color. + Molim izaberite ispravnu boju. + + + Please select a valid country. + Molim izaberite ispravnu državu. + + + Please select a valid currency. + Molim izaberite ispravnu valutu. + + + Please choose a valid date interval. + Molim izaberite ispravan datumski interval. + + + Please enter a valid date and time. + Molim upišite ispravan datum i vreme. + + + Please enter a valid date. + Molim upišite ispravan datum. + + + Please select a valid file. + Molim izaberite ispravnu datoteku. + + + The hidden field is invalid. + Skriveno polje nije ispravno. + + + Please enter an integer. + Molim upišite ceo broj (integer). + + + Please select a valid language. + Molim izaberite ispravan jezik. + + + Please select a valid locale. + Molim izaberite ispravnu lokalizaciju. + + + Please enter a valid money amount. + Molim upišite ispravnu količinu novca. + + + Please enter a number. + Molim upišite broj. + + + The password is invalid. + Ova lozinka nije ispravna. + + + Please enter a percentage value. + Molim upišite procentualnu vrednost. + + + The values do not match. + Date vrednosti se ne poklapaju. + + + Please enter a valid time. + Molim upišite ispravno vreme. + + + Please select a valid timezone. + Molim izaberite ispravnu vremensku zonu. + + + Please enter a valid URL. + Molim upišite ispravan URL. + + + Please enter a valid search term. + Molim upišite ispravan termin za pretragu. + + + Please provide a valid phone number. + Molim navedite ispravan broj telefona. + + + The checkbox has an invalid value. + Polje za potvrdu sadrži neispravnu vrednost. + + + Please enter a valid email address. + Molim upišite ispravnu email adresu. + + + Please select a valid option. + Molim izaberite ispravnu opciju. + + + Please select a valid range. + Molim izaberite ispravan opseg. + + + Please enter a valid week. + Molim upišite ispravnu sedmicu. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.sv.xlf b/lib/symfony/form/Resources/translations/validators.sv.xlf new file mode 100644 index 0000000000..052a569605 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.sv.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Formuläret kan inte innehålla extra fält. + + + The uploaded file was too large. Please try to upload a smaller file. + Den uppladdade filen var för stor. Försök ladda upp en mindre fil. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF-elementet är inte giltigt. Försök att skicka formuläret igen. + + + This value is not a valid HTML5 color. + Värdet är inte en giltig HTML5-färg. + + + Please enter a valid birthdate. + Ange ett giltigt födelsedatum. + + + The selected choice is invalid. + Det valda alternativet är ogiltigt. + + + The collection is invalid. + Den här samlingen är ogiltig. + + + Please select a valid color. + Välj en giltig färg. + + + Please select a valid country. + Välj ett land. + + + Please select a valid currency. + Välj en valuta. + + + Please choose a valid date interval. + Välj ett giltigt datumintervall. + + + Please enter a valid date and time. + Ange ett giltigt datum och tid. + + + Please enter a valid date. + Ange ett giltigt datum. + + + Please select a valid file. + Välj en fil. + + + The hidden field is invalid. + Det dolda fältet är ogiltigt. + + + Please enter an integer. + Ange ett heltal. + + + Please select a valid language. + Välj språk. + + + Please select a valid locale. + Välj plats. + + + Please enter a valid money amount. + Ange en giltig summa pengar. + + + Please enter a number. + Ange en siffra. + + + The password is invalid. + Lösenordet är ogiltigt. + + + Please enter a percentage value. + Ange ett procentuellt värde. + + + The values do not match. + De angivna värdena stämmer inte överens. + + + Please enter a valid time. + Ange en giltig tid. + + + Please select a valid timezone. + Välj en tidszon. + + + Please enter a valid URL. + Ange en giltig URL. + + + Please enter a valid search term. + Ange ett giltigt sökbegrepp. + + + Please provide a valid phone number. + Ange ett giltigt telefonnummer. + + + The checkbox has an invalid value. + Kryssrutan har ett ogiltigt värde. + + + Please enter a valid email address. + Ange en giltig e-postadress. + + + Please select a valid option. + Välj ett alternativ. + + + Please select a valid range. + Välj ett intervall. + + + Please enter a valid week. + Ange en giltig vecka. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.th.xlf b/lib/symfony/form/Resources/translations/validators.th.xlf new file mode 100644 index 0000000000..82d417d955 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.th.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + ฟอร์มนี้ไม่ควรมี extra fields + + + The uploaded file was too large. Please try to upload a smaller file. + ไฟล์ที่อัพโหลดมีขนาดใหญ่เกินไป กรุณาลองอัพโหลดใหม่อีกครั้งด้วยไฟล์ที่มีขนาดเล็กลง + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF token ไม่ถูกต้อง กรุณาลองส่งแบบฟอร์มใหม่ + + + This value is not a valid HTML5 color. + ค่านี้ไม่ใช่ค่าที่ถูกต้องของค่าสี HTML5 + + + Please enter a valid birthdate. + กรุณากรอกวันเดือนปีเกิดที่ถูกต้อง + + + The selected choice is invalid. + ตัวเลือกที่เลิอกไม่ถูกต้อง + + + The collection is invalid. + คอเล็กชั่นไม่ถูกต้อง + + + Please select a valid color. + กรุณาเลือกค่าสีที่ถูกต้อง + + + Please select a valid country. + กรุณาเลือกประเทศที่ถูกต้อง + + + Please select a valid currency. + กรุุณาเลิอกค่าสกุลเงินที่ถูกต้อง + + + Please choose a valid date interval. + กรุณณากรอกช่วงวันที่ที่ถูกต้อง + + + Please enter a valid date and time. + กรุณณากรอกค่าเวลาและวันที่ที่ถูกต้อง + + + Please enter a valid date. + กรุณณากรอกค่าวันที่ที่ถูกต้อง + + + Please select a valid file. + กรุณาเลือกไฟล์ที่ถูกต้อง + + + The hidden field is invalid. + ค่า Hidden field ไม่ถูกต้อง + + + Please enter an integer. + กรุณากรอกตัวเลขจำนวนเต็ม + + + Please select a valid language. + กรุณาเลือกภาษาที่ถูกต้อง + + + Please select a valid locale. + กรุณาเลือกท้องถิ่นที่ถูกต้อง + + + Please enter a valid money amount. + กรุณากรอกจำนวนเงินที่ถูกต้อง + + + Please enter a number. + กรุณากรอกตัวเลข + + + The password is invalid. + รหัสผ่านไม่ถูกต้อง + + + Please enter a percentage value. + กรุณากรอกค่าเปอร์เซ็นต์ + + + The values do not match. + ค่าทั้งสองไม่ตรงกัน + + + Please enter a valid time. + กรุณากรอกค่าเวลาที่ถูกต้อง + + + Please select a valid timezone. + กรุณาเลือกค่าเขตเวลาที่ถูกต้อง + + + Please enter a valid URL. + กรุณากรอก URL ที่ถูกต้อง + + + Please enter a valid search term. + กรุณากรอกคำค้นหาที่ถูกต้อง + + + Please provide a valid phone number. + กรุณากรอกเบอร์โทรศัพท์ที่ถูกต้อง + + + The checkbox has an invalid value. + Checkbox มีค่าที่ไม่ถูกต้อง + + + Please enter a valid email address. + กรุณากรอกที่อยู่อีเมล์ที่ถูกต้อง + + + Please select a valid option. + กรุณาเลือกตัวเลือกที่ถูกต้อง + + + Please select a valid range. + กรุณาเลือกค่าช่วงที่ถูกต้อง + + + Please enter a valid week. + กรุณากรอกค่าสัปดาห์ที่ถูกต้อง + + + + diff --git a/lib/symfony/form/Resources/translations/validators.tl.xlf b/lib/symfony/form/Resources/translations/validators.tl.xlf new file mode 100644 index 0000000000..6aeef41e1e --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.tl.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ang pormang itong ay hindi dapat magkarron ng dagdag na mga patlang. + + + The uploaded file was too large. Please try to upload a smaller file. + Ang ini-upload na file ay masyadong malaki. Pakiulit muling mag-upload ng mas maliit na file. + + + The CSRF token is invalid. Please try to resubmit the form. + Hindi balido ang CSRF token. Maagpasa muli ng isang pang porma. + + + This value is not a valid HTML5 color. + Ang halagang ito ay hindi wastong HTML5 color. + + + Please enter a valid birthdate. + Pakilagay ang tamang petsa ng kapanganakan. + + + The selected choice is invalid. + Ang pinagpiliang sagot ay hindi tama. + + + The collection is invalid. + Hindi balido ang koleksyon. + + + Please select a valid color. + Pakipiliin ang nararapat na kulay. + + + Please select a valid country. + Pakipiliin ang nararapat na bansa. + + + Please select a valid currency. + Pakipiliin ang tamang pananalapi. + + + Please choose a valid date interval. + Piliin ang wastong agwat ng petsa. + + + Please enter a valid date and time. + Piliin ang wastong petsa at oras. + + + Please enter a valid date. + Ilagay ang wastong petsa. + + + Please select a valid file. + Piliin ang balidong file. + + + The hidden field is invalid. + Hindi balido ang field na nakatago. + + + Please enter an integer. + Pakilagay ang integer. + + + Please select a valid language. + Piliin ang nararapat na lengguwahe. + + + Please select a valid locale. + Pakipili ang nararapat na locale. + + + Please enter a valid money amount. + Pakilagay ang tamang halaga ng pera. + + + Please enter a number. + Ilagay ang numero. + + + The password is invalid. + Hindi balido ang password. + + + Please enter a percentage value. + Pakilagay ang tamang porsyento ng halaga. + + + The values do not match. + Hindi tugma ang mga halaga. + + + Please enter a valid time. + Pakilagay ang tamang oras. + + + Please select a valid timezone. + Pakilagay ang tamang sona ng oras. + + + Please enter a valid URL. + Pakilagay ang balidong URL. + + + Please enter a valid search term. + Pakilagay ang balidong katagang sinasaliksik. + + + Please provide a valid phone number. + Pakilagay ang balidong numero ng telepono. + + + The checkbox has an invalid value. + Ang checkbox ay mayroon hindi balidong halaga. + + + Please enter a valid email address. + Pakilagay ang balidong email address. + + + Please select a valid option. + Pakipiliin ang balidong pagpipilian. + + + Please select a valid range. + Pakipilian ang balidong layo. + + + Please enter a valid week. + Pakilagay ang balidong linggo. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.tr.xlf b/lib/symfony/form/Resources/translations/validators.tr.xlf new file mode 100644 index 0000000000..71a469619c --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.tr.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Form ekstra alanlar içeremez. + + + The uploaded file was too large. Please try to upload a smaller file. + Yüklenen dosya boyutu çok yüksek. Lütfen daha küçük bir dosya yüklemeyi deneyin. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF fişi geçersiz. Formu tekrar göndermeyi deneyin. + + + This value is not a valid HTML5 color. + Bu değer, geçerli bir HTML5 rengi değil. + + + Please enter a valid birthdate. + Lütfen geçerli bir doğum tarihi girin. + + + The selected choice is invalid. + Seçilen seçim geçersiz. + + + The collection is invalid. + Koleksiyon geçersiz. + + + Please select a valid color. + Lütfen geçerli bir renk seçin. + + + Please select a valid country. + Lütfen geçerli bir ülke seçin. + + + Please select a valid currency. + Lütfen geçerli bir para birimi seçin. + + + Please choose a valid date interval. + Lütfen geçerli bir tarih aralığı seçin. + + + Please enter a valid date and time. + Lütfen geçerli bir tarih ve saat girin. + + + Please enter a valid date. + Lütfen geçerli bir tarih giriniz. + + + Please select a valid file. + Lütfen geçerli bir dosya seçin. + + + The hidden field is invalid. + Gizli alan geçersiz. + + + Please enter an integer. + Lütfen bir tam sayı girin. + + + Please select a valid language. + Lütfen geçerli bir dil seçin. + + + Please select a valid locale. + Lütfen geçerli bir yerel ayar seçin. + + + Please enter a valid money amount. + Lütfen geçerli bir para tutarı girin. + + + Please enter a number. + Lütfen bir numara giriniz. + + + The password is invalid. + Şifre geçersiz. + + + Please enter a percentage value. + Lütfen bir yüzde değeri girin. + + + The values do not match. + Değerler eşleşmiyor. + + + Please enter a valid time. + Lütfen geçerli bir zaman girin. + + + Please select a valid timezone. + Lütfen geçerli bir saat dilimi seçin. + + + Please enter a valid URL. + Lütfen geçerli bir giriniz URL. + + + Please enter a valid search term. + Lütfen geçerli bir arama terimi girin. + + + Please provide a valid phone number. + lütfen geçerli bir telefon numarası sağlayın. + + + The checkbox has an invalid value. + Onay kutusunda geçersiz bir değer var. + + + Please enter a valid email address. + Lütfen geçerli bir e-posta adresi girin. + + + Please select a valid option. + Lütfen geçerli bir seçenek seçin. + + + Please select a valid range. + Lütfen geçerli bir aralık seçin. + + + Please enter a valid week. + Lütfen geçerli bir hafta girin. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.uk.xlf b/lib/symfony/form/Resources/translations/validators.uk.xlf new file mode 100644 index 0000000000..c6bbca1857 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.uk.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ця форма не повинна містити додаткових полів. + + + The uploaded file was too large. Please try to upload a smaller file. + Завантажений файл занадто великий. Будь ласка, спробуйте завантажити файл меншого розміру. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF значення недопустиме. Будь ласка, спробуйте відправити форму знову. + + + This value is not a valid HTML5 color. + Це значення не є допустимим кольором HTML5. + + + Please enter a valid birthdate. + Будь ласка, введіть дійсну дату народження. + + + The selected choice is invalid. + Обраний варіант недійсний. + + + The collection is invalid. + Колекція недійсна. + + + Please select a valid color. + Будь ласка, оберіть дійсний колір. + + + Please select a valid country. + Будь ласка, оберіть дійсну країну. + + + Please select a valid currency. + Будь ласка, оберіть дійсну валюту. + + + Please choose a valid date interval. + Будь ласка, оберіть дійсний інтервал дати. + + + Please enter a valid date and time. + Будь ласка, введіть дійсну дату та час. + + + Please enter a valid date. + Будь ласка, введіть дійсну дату. + + + Please select a valid file. + Будь ласка, оберіть дійсний файл. + + + The hidden field is invalid. + Приховане поле недійсне. + + + Please enter an integer. + Будь ласка, введіть ціле число. + + + Please select a valid language. + Будь ласка, оберіть дійсну мову. + + + Please select a valid locale. + Будь ласка, оберіть дійсну локаль. + + + Please enter a valid money amount. + Будь ласка, введіть дійсну суму грошей. + + + Please enter a number. + Будь ласка, введіть число. + + + The password is invalid. + Пароль недійсний. + + + Please enter a percentage value. + Будь ласка, введіть процентне значення. + + + The values do not match. + Значення не збігаються. + + + Please enter a valid time. + Будь ласка, введіть дійсний час. + + + Please select a valid timezone. + Будь ласка, оберіть дійсний часовий пояс. + + + Please enter a valid URL. + Будь ласка, введіть дійсну URL-адресу. + + + Please enter a valid search term. + Будь ласка, введіть дійсний пошуковий термін. + + + Please provide a valid phone number. + Будь ласка, введіть дійсний номер телефону. + + + The checkbox has an invalid value. + Прапорець має недійсне значення. + + + Please enter a valid email address. + Будь ласка, введіть дійсну адресу електронної пошти. + + + Please select a valid option. + Будь ласка, оберіть дійсний варіант. + + + Please select a valid range. + Будь ласка, оберіть дійсний діапазон. + + + Please enter a valid week. + Будь ласка, введіть дійсний тиждень. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.ur.xlf b/lib/symfony/form/Resources/translations/validators.ur.xlf new file mode 100644 index 0000000000..42b891bbf3 --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.ur.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + اس فارم میں اضافی فیلڈز نہیں ہونی چاہئیں + + + The uploaded file was too large. Please try to upload a smaller file. + اپ لوڈ کردھ فائل بہت بڑی تھی۔ براہ کرم ایک چھوٹی فائل اپ لوڈ کرنے کی کوشش کریں + + + The CSRF token is invalid. Please try to resubmit the form. + ٹوکن غلط ہے۔ براۓ کرم فارم کو دوبارہ جمع کرانے کی کوشش کریں CSRF + + + This value is not a valid HTML5 color. + ر نگ نھیں ھےHTML یھ ولیو در ست + + + Please enter a valid birthdate. + براۓ کرم درست تاریخ پیدائش درج کریں + + + The selected choice is invalid. + منتخب کردہ انتخاب غلط ہے + + + The collection is invalid. + یھ مجموعہ غلط ہے + + + Please select a valid color. + براۓ کرم ایک درست رنگ منتخب کریں + + + Please select a valid country. + براۓ کرم ایک درست ملک منتخب کریں + + + Please select a valid currency. + براۓ کرم ایک درست کرنسی منتخب کریں + + + Please choose a valid date interval. + براۓ کرم ایک درست تاریخی وقفھہ منتخب کریں + + + Please enter a valid date and time. + براۓ کرم ایک درست تاریخ اور وقت درج کریں + + + Please enter a valid date. + براۓ کرم ایک درست تاریخ درج کریں + + + Please select a valid file. + براۓ کرم ایک درست فائل منتخب کریں + + + The hidden field is invalid. + پوشیدھہ فیلڈ غلط ہے + + + Please enter an integer. + براۓ کرم ایک عدد درج کریں + + + Please select a valid language. + براۓ کرم ایک درست زبان منتخب کریں + + + Please select a valid locale. + براۓ کرم ایک درست مقام منتخب کریں + + + Please enter a valid money amount. + براۓ کرم ایک درست رقم درج کریں + + + Please enter a number. + براۓ کرم ایک نمبر درج کریں + + + The password is invalid. + پاس ورڈ غلط ہے + + + Please enter a percentage value. + براہ کرم فیصد کی ويلو درج کریں + + + The values do not match. + ويليوذ ٹھيک نہیں ہیں + + + Please enter a valid time. + براۓ کرم ایک درست وقت درج کریں + + + Please select a valid timezone. + براۓ کرم ایک درست ٹائم زون منتخب کریں + + + Please enter a valid URL. + براۓ کرم ایک درست ادريس درج کریں + + + Please enter a valid search term. + براۓ کرم ایک درست ويلو تلاش کيلۓ درج کریں + + + Please provide a valid phone number. + براۓ کرم ایک درست فون نمبر فراہم کریں + + + The checkbox has an invalid value. + چیک باکس میں ایک غلط ويلو ہے + + + Please enter a valid email address. + براۓ مہربانی قابل قبول ای میل ایڈریس لکھیں + + + Please select a valid option. + براۓ کرم ایک درست آپشن منتخب کریں + + + Please select a valid range. + براۓ کرم ایک درست رینج منتخب کریں + + + Please enter a valid week. + براۓ کرم ایک درست ہفتہ درج کریں + + + + diff --git a/lib/symfony/form/Resources/translations/validators.uz.xlf b/lib/symfony/form/Resources/translations/validators.uz.xlf new file mode 100644 index 0000000000..86be2379cb --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.uz.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Ushbu fo'rmada qo'shimcha maydonlar bo'lmasligi kerak. + + + The uploaded file was too large. Please try to upload a smaller file. + Yuklab olingan fayl juda katta. Iltimos, kichikroq faylni yuklashga harakat qiling. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF qiymati yaroqsiz. Fo'rmani qayta yuborishga harakat qiling. + + + This value is not a valid HTML5 color. + Qiymat noto'g'ri, HTML5 rangi emas. + + + Please enter a valid birthdate. + Iltimos, tug'ilgan kuningizni to'g'ri kiriting. + + + The selected choice is invalid. + Tanlangan parametr noto'g'ri. + + + The collection is invalid. + Kolleksiya noto'g'ri + + + Please select a valid color. + Iltimos, to'g'ri rang tanlang. + + + Please select a valid country. + Iltimos, to'g'ri mamlakatni tanlang. + + + Please select a valid currency. + Iltimos, to'g'ri valyutani tanlang. + + + Please choose a valid date interval. + Iltimos, to'g'ri sana oralig'ini tanlang. + + + Please enter a valid date and time. + Iltimos, to'g'ri sana va vaqtni kiriting. + + + Please enter a valid date. + Iltimos, to'g'ri sanani kiriting. + + + Please select a valid file. + Iltimos, to'g'ri faylni tanlang. + + + The hidden field is invalid. + Yashirin maydon qiymati yaroqsiz. + + + Please enter an integer. + Iltimos, butun son kiriting. + + + Please select a valid language. + Iltimos, to'g'ri tilni tanlang. + + + Please select a valid locale. + Iltimos, to'g'ri localni tanlang. + + + Please enter a valid money amount. + Iltimos, tegishli miqdordagi pulni kiriting. + + + Please enter a number. + Iltimos, raqam kiriting. + + + The password is invalid. + Parol noto'g'ri. + + + Please enter a percentage value. + Iltimos, foyizli qiymat kiriting. + + + The values do not match. + Qiymatlar mos kelmaydi. + + + Please enter a valid time. + Iltimos, to'g'ri vaqtni tanlang. + + + Please select a valid timezone. + Iltimos, to'g'ri vaqt zonasini tanlang. + + + Please enter a valid URL. + Iltimos, to'g'ri URL kiriting. + + + Please enter a valid search term. + Iltimos, to'g'ri qidiruv so'zini kiriting. + + + Please provide a valid phone number. + Iltimos, to'g'ri telefon raqamini kiriting. + + + The checkbox has an invalid value. + Belgilash katagida yaroqsiz qiymat mavjud. + + + Please enter a valid email address. + Iltimos, to'g'ri email kiriting. + + + Please select a valid option. + Iltimos, yaroqli variantni tanlang. + + + Please select a valid range. + Iltimos, yaroqli oraliqni tanlang. + + + Please enter a valid week. + Iltimos, haqiqiy haftani kiriting. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.vi.xlf b/lib/symfony/form/Resources/translations/validators.vi.xlf new file mode 100644 index 0000000000..92171c055a --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.vi.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + Mẫu này không nên chứa trường mở rộng. + + + The uploaded file was too large. Please try to upload a smaller file. + Tập tin tải lên quá lớn. Vui lòng thử lại với tập tin nhỏ hơn. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF token không hợp lệ. Vui lòng thử lại. + + + This value is not a valid HTML5 color. + Giá trị này không phải là màu HTML5 hợp lệ. + + + Please enter a valid birthdate. + Vui lòng nhập ngày sinh hợp lệ. + + + The selected choice is invalid. + Lựa chọn không hợp lệ. + + + The collection is invalid. + Danh sách không hợp lệ. + + + Please select a valid color. + Vui lòng chọn một màu hợp lệ. + + + Please select a valid country. + Vui lòng chọn đất nước hợp lệ. + + + Please select a valid currency. + Vui lòng chọn tiền tệ hợp lệ. + + + Please choose a valid date interval. + Vui lòng chọn một khoảng thời gian hợp lệ. + + + Please enter a valid date and time. + Vui lòng nhập ngày và thời gian hợp lệ. + + + Please enter a valid date. + Vui lòng nhập ngày hợp lệ. + + + Please select a valid file. + Vui lòng chọn tệp hợp lệ. + + + The hidden field is invalid. + Phạm vi ẩn không hợp lệ. + + + Please enter an integer. + Vui lòng nhập một số nguyên. + + + Please select a valid language. + Vui lòng chọn ngôn ngữ hợp lệ. + + + Please select a valid locale. + Vui lòng chọn miền hợp lệ. + + + Please enter a valid money amount. + Vui lòng nhập một khoảng tiền hợp lệ. + + + Please enter a number. + Vui lòng nhập một con số. + + + The password is invalid. + Mật khẩu không hợp lệ. + + + Please enter a percentage value. + Vui lòng nhập một giá trị phần trăm. + + + The values do not match. + Các giá trị không phù hợp. + + + Please enter a valid time. + Vui lòng nhập thời gian hợp lệ. + + + Please select a valid timezone. + Vui lòng chọn múi giờ hợp lệ. + + + Please enter a valid URL. + Vui lòng nhập một URL hợp lệ. + + + Please enter a valid search term. + Vui lòng nhập chuỗi tìm kiếm hợp lệ. + + + Please provide a valid phone number. + Vui lòng cung cấp số điện thoại hợp lệ. + + + The checkbox has an invalid value. + Hộp kiểm có một giá trị không hợp lệ. + + + Please enter a valid email address. + Vui lòng nhập địa chỉ email hợp lệ. + + + Please select a valid option. + Vui lòng chọn một phương án hợp lệ. + + + Please select a valid range. + Vui lòng nhập một phạm vi hợp lệ. + + + Please enter a valid week. + Vui lòng nhập một tuần hợp lệ. + + + + diff --git a/lib/symfony/form/Resources/translations/validators.zh_CN.xlf b/lib/symfony/form/Resources/translations/validators.zh_CN.xlf new file mode 100644 index 0000000000..a1469b798c --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.zh_CN.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + 该表单中不可有额外字段. + + + The uploaded file was too large. Please try to upload a smaller file. + 上传文件太大, 请重新尝试上传一个较小的文件. + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF 验证符无效, 请重新提交. + + + This value is not a valid HTML5 color. + 该数值不是个有效的 HTML5 颜色。 + + + Please enter a valid birthdate. + 请输入有效的生日日期。 + + + The selected choice is invalid. + 所选的选项无效。 + + + The collection is invalid. + 集合无效。 + + + Please select a valid color. + 请选择有效的颜色。 + + + Please select a valid country. + 请选择有效的国家。 + + + Please select a valid currency. + 请选择有效的货币。 + + + Please choose a valid date interval. + 请选择有效的日期间隔。 + + + Please enter a valid date and time. + 请输入有效的日期与时间。 + + + Please enter a valid date. + 请输入有效的日期。 + + + Please select a valid file. + 请选择有效的文件。 + + + The hidden field is invalid. + 隐藏字段无效。 + + + Please enter an integer. + 请输入整数。 + + + Please select a valid language. + 请选择有效的语言。 + + + Please select a valid locale. + 请选择有效的语言环境。 + + + Please enter a valid money amount. + 请输入正确的金额。 + + + Please enter a number. + 请输入数字。 + + + The password is invalid. + 密码无效。 + + + Please enter a percentage value. + 请输入百分比值。 + + + The values do not match. + 数值不匹配。 + + + Please enter a valid time. + 请输入有效的时间。 + + + Please select a valid timezone. + 请选择有效的时区。 + + + Please enter a valid URL. + 请输入有效的网址。 + + + Please enter a valid search term. + 请输入有效的搜索词。 + + + Please provide a valid phone number. + 请提供有效的手机号码。 + + + The checkbox has an invalid value. + 无效的选框值。 + + + Please enter a valid email address. + 请输入有效的电子邮件地址。 + + + Please select a valid option. + 请选择有效的选项。 + + + Please select a valid range. + 请选择有效的范围。 + + + Please enter a valid week. + 请输入有效的星期。 + + + + diff --git a/lib/symfony/form/Resources/translations/validators.zh_TW.xlf b/lib/symfony/form/Resources/translations/validators.zh_TW.xlf new file mode 100644 index 0000000000..0a76ab7a7b --- /dev/null +++ b/lib/symfony/form/Resources/translations/validators.zh_TW.xlf @@ -0,0 +1,139 @@ + + + + + + This form should not contain extra fields. + 此表單不應包含其他欄位。 + + + The uploaded file was too large. Please try to upload a smaller file. + 上傳的檔案過大。請嘗試上傳較小的檔案。 + + + The CSRF token is invalid. Please try to resubmit the form. + CSRF token 無效。請重新提交表單。 + + + This value is not a valid HTML5 color. + 這個數值不是有效的 HTML5 顏色。 + + + Please enter a valid birthdate. + 請輸入有效的出生日期。 + + + The selected choice is invalid. + 選取的選項無效。 + + + The collection is invalid. + 這個集合無效。 + + + Please select a valid color. + 請選擇有效的顏色。 + + + Please select a valid country. + 請選擇有效的國家。 + + + Please select a valid currency. + 請選擇有效的貨幣。 + + + Please choose a valid date interval. + 請選擇有效的日期區間。 + + + Please enter a valid date and time. + 請輸入有效的日期和時間。 + + + Please enter a valid date. + 請輸入有效的日期。 + + + Please select a valid file. + 請選擇有效的檔案。 + + + The hidden field is invalid. + 隱藏欄位無效。 + + + Please enter an integer. + 請輸入整數。 + + + Please select a valid language. + 請選擇有效的語言。 + + + Please select a valid locale. + 請選擇有效的語系。 + + + Please enter a valid money amount. + 請輸入有效的金額。 + + + Please enter a number. + 請輸入數字。 + + + The password is invalid. + 密碼無效。 + + + Please enter a percentage value. + 請輸入百分比數值。 + + + The values do not match. + 數值不相符。 + + + Please enter a valid time. + 請輸入有效的時間。 + + + Please select a valid timezone. + 請選擇有效的時區。 + + + Please enter a valid URL. + 請輸入有效的 URL。 + + + Please enter a valid search term. + 請輸入有效的搜尋關鍵字。 + + + Please provide a valid phone number. + 請提供有效的電話號碼。 + + + The checkbox has an invalid value. + 核取方塊上有無效的值。 + + + Please enter a valid email address. + 請輸入有效的電子郵件地址。 + + + Please select a valid option. + 請選擇有效的選項。 + + + Please select a valid range. + 請選擇有效的範圍。 + + + Please enter a valid week. + 請輸入有效的星期。 + + + + diff --git a/lib/symfony/form/ReversedTransformer.php b/lib/symfony/form/ReversedTransformer.php new file mode 100644 index 0000000000..8572672369 --- /dev/null +++ b/lib/symfony/form/ReversedTransformer.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * Reverses a transformer. + * + * When the transform() method is called, the reversed transformer's + * reverseTransform() method is called and vice versa. + * + * @author Bernhard Schussek + */ +class ReversedTransformer implements DataTransformerInterface +{ + protected $reversedTransformer; + + public function __construct(DataTransformerInterface $reversedTransformer) + { + $this->reversedTransformer = $reversedTransformer; + } + + public function transform(mixed $value): mixed + { + return $this->reversedTransformer->reverseTransform($value); + } + + public function reverseTransform(mixed $value): mixed + { + return $this->reversedTransformer->transform($value); + } +} diff --git a/lib/symfony/form/SubmitButton.php b/lib/symfony/form/SubmitButton.php new file mode 100644 index 0000000000..37ce141d27 --- /dev/null +++ b/lib/symfony/form/SubmitButton.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\Form; + +/** + * A button that submits the form. + * + * @author Bernhard Schussek + */ +class SubmitButton extends Button implements ClickableInterface +{ + private bool $clicked = false; + + public function isClicked(): bool + { + return $this->clicked; + } + + /** + * Submits data to the button. + * + * @return $this + * + * @throws Exception\AlreadySubmittedException if the form has already been submitted + */ + public function submit(array|string|null $submittedData, bool $clearMissing = true): static + { + if ($this->getConfig()->getDisabled()) { + $this->clicked = false; + + return $this; + } + + parent::submit($submittedData, $clearMissing); + + $this->clicked = null !== $submittedData; + + return $this; + } +} diff --git a/lib/symfony/form/SubmitButtonBuilder.php b/lib/symfony/form/SubmitButtonBuilder.php new file mode 100644 index 0000000000..b98398f90f --- /dev/null +++ b/lib/symfony/form/SubmitButtonBuilder.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * A builder for {@link SubmitButton} instances. + * + * @author Bernhard Schussek + */ +class SubmitButtonBuilder extends ButtonBuilder +{ + /** + * Creates the button. + */ + public function getForm(): SubmitButton + { + return new SubmitButton($this->getFormConfig()); + } +} diff --git a/lib/symfony/form/SubmitButtonTypeInterface.php b/lib/symfony/form/SubmitButtonTypeInterface.php new file mode 100644 index 0000000000..f7ac13f7ec --- /dev/null +++ b/lib/symfony/form/SubmitButtonTypeInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * A type that should be converted into a {@link SubmitButton} instance. + * + * @author Bernhard Schussek + */ +interface SubmitButtonTypeInterface extends FormTypeInterface +{ +} diff --git a/lib/symfony/form/Test/FormBuilderInterface.php b/lib/symfony/form/Test/FormBuilderInterface.php new file mode 100644 index 0000000000..185a8a12d6 --- /dev/null +++ b/lib/symfony/form/Test/FormBuilderInterface.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Test; + +use Symfony\Component\Form\FormBuilderInterface as BaseFormBuilderInterface; + +interface FormBuilderInterface extends \Iterator, BaseFormBuilderInterface +{ +} diff --git a/lib/symfony/form/Test/FormIntegrationTestCase.php b/lib/symfony/form/Test/FormIntegrationTestCase.php new file mode 100644 index 0000000000..5bf37fd48a --- /dev/null +++ b/lib/symfony/form/Test/FormIntegrationTestCase.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\Form\Test; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\Forms; + +/** + * @author Bernhard Schussek + */ +abstract class FormIntegrationTestCase extends TestCase +{ + protected FormFactoryInterface $factory; + + protected function setUp(): void + { + $this->factory = Forms::createFormFactoryBuilder() + ->addExtensions($this->getExtensions()) + ->addTypeExtensions($this->getTypeExtensions()) + ->addTypes($this->getTypes()) + ->addTypeGuessers($this->getTypeGuessers()) + ->getFormFactory(); + } + + protected function getExtensions() + { + return []; + } + + protected function getTypeExtensions() + { + return []; + } + + protected function getTypes() + { + return []; + } + + protected function getTypeGuessers() + { + return []; + } +} diff --git a/lib/symfony/form/Test/FormInterface.php b/lib/symfony/form/Test/FormInterface.php new file mode 100644 index 0000000000..4af4603087 --- /dev/null +++ b/lib/symfony/form/Test/FormInterface.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Test; + +use Symfony\Component\Form\FormInterface as BaseFormInterface; + +interface FormInterface extends \Iterator, BaseFormInterface +{ +} diff --git a/lib/symfony/form/Test/FormPerformanceTestCase.php b/lib/symfony/form/Test/FormPerformanceTestCase.php new file mode 100644 index 0000000000..2f7c307685 --- /dev/null +++ b/lib/symfony/form/Test/FormPerformanceTestCase.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Test; + +use Symfony\Component\Form\Test\Traits\RunTestTrait; + +/** + * Base class for performance tests. + * + * Copied from Doctrine 2's OrmPerformanceTestCase. + * + * @author robo + * @author Bernhard Schussek + */ +abstract class FormPerformanceTestCase extends FormIntegrationTestCase +{ + use RunTestTrait; + + /** + * @var int + */ + protected $maxRunningTime = 0; + + private function doRunTest(): mixed + { + $s = microtime(true); + $result = parent::runTest(); + $time = microtime(true) - $s; + + if (0 != $this->maxRunningTime && $time > $this->maxRunningTime) { + $this->fail(\sprintf('expected running time: <= %s but was: %s', $this->maxRunningTime, $time)); + } + + $this->expectNotToPerformAssertions(); + + return $result; + } + + /** + * @throws \InvalidArgumentException + */ + public function setMaxRunningTime(int $maxRunningTime) + { + if ($maxRunningTime < 0) { + throw new \InvalidArgumentException(); + } + + $this->maxRunningTime = $maxRunningTime; + } + + public function getMaxRunningTime(): int + { + return $this->maxRunningTime; + } +} diff --git a/lib/symfony/form/Test/Traits/RunTestTrait.php b/lib/symfony/form/Test/Traits/RunTestTrait.php new file mode 100644 index 0000000000..17204b9670 --- /dev/null +++ b/lib/symfony/form/Test/Traits/RunTestTrait.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Test\Traits; + +use PHPUnit\Framework\TestCase; + +if ((new \ReflectionMethod(TestCase::class, 'runTest'))->hasReturnType()) { + // PHPUnit 10 + /** @internal */ + trait RunTestTrait + { + protected function runTest(): mixed + { + return $this->doRunTest(); + } + } +} else { + // PHPUnit 9 + /** @internal */ + trait RunTestTrait + { + protected function runTest() + { + return $this->doRunTest(); + } + } +} diff --git a/lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php b/lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php new file mode 100644 index 0000000000..27c791e579 --- /dev/null +++ b/lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Test\Traits; + +use Symfony\Component\Form\Extension\Validator\ValidatorExtension; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +trait ValidatorExtensionTrait +{ + /** + * @var ValidatorInterface|null + */ + protected $validator; + + protected function getValidatorExtension(): ValidatorExtension + { + if (!interface_exists(ValidatorInterface::class)) { + throw new \Exception('In order to use the "ValidatorExtensionTrait", the symfony/validator component must be installed.'); + } + + if (!$this instanceof TypeTestCase) { + throw new \Exception(\sprintf('The trait "ValidatorExtensionTrait" can only be added to a class that extends "%s".', TypeTestCase::class)); + } + + $this->validator = $this->createMock(ValidatorInterface::class); + $metadata = $this->getMockBuilder(ClassMetadata::class)->setConstructorArgs([''])->onlyMethods(['addPropertyConstraint'])->getMock(); + $this->validator->expects($this->any())->method('getMetadataFor')->willReturn($metadata); + $this->validator->expects($this->any())->method('validate')->willReturn(new ConstraintViolationList()); + + return new ValidatorExtension($this->validator, false); + } +} diff --git a/lib/symfony/form/Test/TypeTestCase.php b/lib/symfony/form/Test/TypeTestCase.php new file mode 100644 index 0000000000..ac8eb9baa4 --- /dev/null +++ b/lib/symfony/form/Test/TypeTestCase.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\Form\Test; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; + +abstract class TypeTestCase extends FormIntegrationTestCase +{ + /** + * @var FormBuilder + */ + protected $builder; + + /** + * @var EventDispatcherInterface + */ + protected $dispatcher; + + protected function setUp(): void + { + parent::setUp(); + + $this->dispatcher = $this->createMock(EventDispatcherInterface::class); + $this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory); + } + + protected function getExtensions() + { + $extensions = []; + + if (\in_array(ValidatorExtensionTrait::class, class_uses($this))) { + $extensions[] = $this->getValidatorExtension(); + } + + return $extensions; + } + + public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual) + { + self::assertEquals($expected->format('c'), $actual->format('c')); + } + + public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual) + { + self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS')); + } +} diff --git a/lib/symfony/form/Util/FormUtil.php b/lib/symfony/form/Util/FormUtil.php new file mode 100644 index 0000000000..1a5cd3b15e --- /dev/null +++ b/lib/symfony/form/Util/FormUtil.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * @author Bernhard Schussek + */ +class FormUtil +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Returns whether the given data is empty. + * + * This logic is reused multiple times throughout the processing of + * a form and needs to be consistent. PHP keyword `empty` cannot + * be used as it also considers 0 and "0" to be empty. + */ + public static function isEmpty(mixed $data): bool + { + // Should not do a check for [] === $data!!! + // This method is used in occurrences where arrays are + // not considered to be empty, ever. + return null === $data || '' === $data; + } + + /** + * Recursively replaces or appends elements of the first array with elements + * of second array. If the key is an integer, the values will be appended to + * the new array; otherwise, the value from the second array will replace + * the one from the first array. + */ + public static function mergeParamsAndFiles(array $params, array $files): array + { + $isFilesList = array_is_list($files); + + foreach ($params as $key => $value) { + if (\is_array($value) && \is_array($files[$key] ?? null)) { + $params[$key] = self::mergeParamsAndFiles($value, $files[$key]); + unset($files[$key]); + } + } + + if (!$isFilesList) { + return array_replace($params, $files); + } + + foreach ($files as $value) { + $params[] = $value; + } + + return $params; + } +} diff --git a/lib/symfony/form/Util/InheritDataAwareIterator.php b/lib/symfony/form/Util/InheritDataAwareIterator.php new file mode 100644 index 0000000000..26a2135223 --- /dev/null +++ b/lib/symfony/form/Util/InheritDataAwareIterator.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * Iterator that traverses an array of forms. + * + * Contrary to \ArrayIterator, this iterator recognizes changes in the original + * array during iteration. + * + * You can wrap the iterator into a {@link \RecursiveIteratorIterator} in order to + * enter any child form that inherits its parent's data and iterate the children + * of that form as well. + * + * @author Bernhard Schussek + */ +class InheritDataAwareIterator extends \IteratorIterator implements \RecursiveIterator +{ + public function getChildren(): static + { + return new static($this->current()); + } + + public function hasChildren(): bool + { + return (bool) $this->current()->getConfig()->getInheritData(); + } +} diff --git a/lib/symfony/form/Util/OptionsResolverWrapper.php b/lib/symfony/form/Util/OptionsResolverWrapper.php new file mode 100644 index 0000000000..51cba4e08f --- /dev/null +++ b/lib/symfony/form/Util/OptionsResolverWrapper.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\Form\Util; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Yonel Ceruto + * + * @internal + */ +class OptionsResolverWrapper extends OptionsResolver +{ + private array $undefined = []; + + /** + * @return $this + */ + public function setNormalizer(string $option, \Closure $normalizer): static + { + try { + parent::setNormalizer($option, $normalizer); + } catch (UndefinedOptionsException) { + $this->undefined[$option] = true; + } + + return $this; + } + + /** + * @return $this + */ + public function setAllowedValues(string $option, mixed $allowedValues): static + { + try { + parent::setAllowedValues($option, $allowedValues); + } catch (UndefinedOptionsException) { + $this->undefined[$option] = true; + } + + return $this; + } + + /** + * @return $this + */ + public function addAllowedValues(string $option, mixed $allowedValues): static + { + try { + parent::addAllowedValues($option, $allowedValues); + } catch (UndefinedOptionsException) { + $this->undefined[$option] = true; + } + + return $this; + } + + /** + * @param string|array $allowedTypes + * + * @return $this + */ + public function setAllowedTypes(string $option, $allowedTypes): static + { + try { + parent::setAllowedTypes($option, $allowedTypes); + } catch (UndefinedOptionsException) { + $this->undefined[$option] = true; + } + + return $this; + } + + /** + * @param string|array $allowedTypes + * + * @return $this + */ + public function addAllowedTypes(string $option, $allowedTypes): static + { + try { + parent::addAllowedTypes($option, $allowedTypes); + } catch (UndefinedOptionsException) { + $this->undefined[$option] = true; + } + + return $this; + } + + public function resolve(array $options = []): array + { + throw new AccessException('Resolve options is not supported.'); + } + + public function getUndefinedOptions(): array + { + return array_keys($this->undefined); + } +} diff --git a/lib/symfony/form/Util/OrderedHashMap.php b/lib/symfony/form/Util/OrderedHashMap.php new file mode 100644 index 0000000000..f92be10a4b --- /dev/null +++ b/lib/symfony/form/Util/OrderedHashMap.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * A hash map which keeps track of deletions and additions. + * + * Like in associative arrays, elements can be mapped to integer or string keys. + * Unlike associative arrays, the map keeps track of the order in which keys + * were added and removed. This order is reflected during iteration. + * + * The map supports concurrent modification during iteration. That means that + * you can insert and remove elements from within a foreach loop and the + * iterator will reflect those changes accordingly. + * + * While elements that are added during the loop are recognized by the iterator, + * changed elements are not. Otherwise the loop could be infinite if each loop + * changes the current element: + * + * $map = new OrderedHashMap(); + * $map[1] = 1; + * $map[2] = 2; + * $map[3] = 3; + * + * foreach ($map as $index => $value) { + * echo "$index: $value\n" + * if (1 === $index) { + * $map[1] = 4; + * $map[] = 5; + * } + * } + * + * print_r(iterator_to_array($map)); + * + * // => 1: 1 + * // 2: 2 + * // 3: 3 + * // 4: 5 + * // Array + * // ( + * // [1] => 4 + * // [2] => 2 + * // [3] => 3 + * // [4] => 5 + * // ) + * + * The map also supports multiple parallel iterators. That means that you can + * nest foreach loops without affecting each other's iteration: + * + * foreach ($map as $index => $value) { + * foreach ($map as $index2 => $value2) { + * // ... + * } + * } + * + * @author Bernhard Schussek + * + * @template TValue + * + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable +{ + /** + * The elements of the map, indexed by their keys. + * + * @var TValue[] + */ + private array $elements = []; + + /** + * The keys of the map in the order in which they were inserted or changed. + * + * @var list + */ + private array $orderedKeys = []; + + /** + * References to the cursors of all open iterators. + * + * @var array + */ + private array $managedCursors = []; + + /** + * Creates a new map. + * + * @param TValue[] $elements The elements to insert initially + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + // the explicit string type-cast is necessary as digit-only keys would be returned as integers otherwise + $this->orderedKeys = array_map(strval(...), array_keys($elements)); + } + + public function offsetExists(mixed $key): bool + { + return isset($this->elements[$key]); + } + + public function offsetGet(mixed $key): mixed + { + if (!isset($this->elements[$key])) { + throw new \OutOfBoundsException(\sprintf('The offset "%s" does not exist.', $key)); + } + + return $this->elements[$key]; + } + + public function offsetSet(mixed $key, mixed $value): void + { + if (null === $key || !isset($this->elements[$key])) { + if (null === $key) { + $key = [] === $this->orderedKeys + // If the array is empty, use 0 as key + ? 0 + // Imitate PHP behavior of generating a key that equals + // the highest existing integer key + 1 + : 1 + (int) max($this->orderedKeys); + } + + $this->orderedKeys[] = (string) $key; + } + + $this->elements[$key] = $value; + } + + public function offsetUnset(mixed $key): void + { + if (false !== ($position = array_search((string) $key, $this->orderedKeys))) { + array_splice($this->orderedKeys, $position, 1); + unset($this->elements[$key]); + + foreach ($this->managedCursors as $i => $cursor) { + if ($cursor >= $position) { + --$this->managedCursors[$i]; + } + } + } + } + + public function getIterator(): \Traversable + { + return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors); + } + + public function count(): int + { + return \count($this->elements); + } +} diff --git a/lib/symfony/form/Util/OrderedHashMapIterator.php b/lib/symfony/form/Util/OrderedHashMapIterator.php new file mode 100644 index 0000000000..9ec6d9aa50 --- /dev/null +++ b/lib/symfony/form/Util/OrderedHashMapIterator.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * Iterator for {@link OrderedHashMap} objects. + * + * @author Bernhard Schussek + * + * @internal + * + * @template-covariant TValue + * + * @implements \Iterator + */ +class OrderedHashMapIterator implements \Iterator +{ + /** @var TValue[] */ + private array $elements; + /** @var list */ + private array $orderedKeys; + private int $cursor = 0; + private int $cursorId; + /** @var array */ + private array $managedCursors; + private ?string $key = null; + /** @var TValue|null */ + private mixed $current = null; + + /** + * @param TValue[] $elements The elements of the map, indexed by their + * keys + * @param list $orderedKeys The keys of the map in the order in which + * they should be iterated + * @param array $managedCursors An array from which to reference the + * iterator's cursor as long as it is alive. + * This array is managed by the corresponding + * {@link OrderedHashMap} instance to support + * recognizing the deletion of elements. + */ + public function __construct(array &$elements, array &$orderedKeys, array &$managedCursors) + { + $this->elements = &$elements; + $this->orderedKeys = &$orderedKeys; + $this->managedCursors = &$managedCursors; + $this->cursorId = \count($managedCursors); + + $this->managedCursors[$this->cursorId] = &$this->cursor; + } + + public function __serialize(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __unserialize(array $data): void + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + /** + * Removes the iterator's cursors from the managed cursors of the + * corresponding {@link OrderedHashMap} instance. + */ + public function __destruct() + { + // Use array_splice() instead of unset() to prevent holes in the + // array indices, which would break the initialization of $cursorId + array_splice($this->managedCursors, $this->cursorId, 1); + } + + public function current(): mixed + { + return $this->current; + } + + public function next(): void + { + ++$this->cursor; + + if (isset($this->orderedKeys[$this->cursor])) { + $this->key = $this->orderedKeys[$this->cursor]; + $this->current = $this->elements[$this->key]; + } else { + $this->key = null; + $this->current = null; + } + } + + public function key(): mixed + { + if (null === $this->key) { + return null; + } + + return $this->key; + } + + public function valid(): bool + { + return null !== $this->key; + } + + public function rewind(): void + { + $this->cursor = 0; + + if (isset($this->orderedKeys[0])) { + $this->key = $this->orderedKeys[0]; + $this->current = $this->elements[$this->key]; + } else { + $this->key = null; + $this->current = null; + } + } +} diff --git a/lib/symfony/form/Util/ServerParams.php b/lib/symfony/form/Util/ServerParams.php new file mode 100644 index 0000000000..e53faaa8ac --- /dev/null +++ b/lib/symfony/form/Util/ServerParams.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * @author Bernhard Schussek + */ +class ServerParams +{ + private ?RequestStack $requestStack; + + public function __construct(?RequestStack $requestStack = null) + { + $this->requestStack = $requestStack; + } + + /** + * Returns true if the POST max size has been exceeded in the request. + */ + public function hasPostMaxSizeBeenExceeded(): bool + { + $contentLength = $this->getContentLength(); + $maxContentLength = $this->getPostMaxSize(); + + return $maxContentLength && $contentLength > $maxContentLength; + } + + /** + * Returns maximum post size in bytes. + */ + public function getPostMaxSize(): int|float|null + { + $iniMax = strtolower($this->getNormalizedIniPostMaxSize()); + + if ('' === $iniMax) { + return null; + } + + $max = ltrim($iniMax, '+'); + if (str_starts_with($max, '0x')) { + $max = \intval($max, 16); + } elseif (str_starts_with($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($iniMax, -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + + return $max; + } + + /** + * Returns the normalized "post_max_size" ini setting. + */ + public function getNormalizedIniPostMaxSize(): string + { + return strtoupper(trim(\ini_get('post_max_size'))); + } + + /** + * Returns the content length of the request. + */ + public function getContentLength(): mixed + { + if (null !== $this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) { + return $request->server->get('CONTENT_LENGTH'); + } + + return isset($_SERVER['CONTENT_LENGTH']) + ? (int) $_SERVER['CONTENT_LENGTH'] + : null; + } +} diff --git a/lib/symfony/form/Util/StringUtil.php b/lib/symfony/form/Util/StringUtil.php new file mode 100644 index 0000000000..45a50c1adc --- /dev/null +++ b/lib/symfony/form/Util/StringUtil.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * @author Issei Murasawa + * @author Bernhard Schussek + */ +class StringUtil +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Returns the trimmed data. + */ + public static function trim(string $string): string + { + if (null !== $result = @preg_replace('/^[\pZ\p{Cc}\p{Cf}]+|[\pZ\p{Cc}\p{Cf}]+$/u', '', $string)) { + return $result; + } + + return trim($string); + } + + /** + * Converts a fully-qualified class name to a block prefix. + * + * @param string $fqcn The fully-qualified class name + */ + public static function fqcnToBlockPrefix(string $fqcn): ?string + { + // Non-greedy ("+?") to match "type" suffix, if present + if (preg_match('~([^\\\\]+?)(type)?$~i', $fqcn, $matches)) { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $matches[1])); + } + + return null; + } +} diff --git a/lib/symfony/form/composer.json b/lib/symfony/form/composer.json new file mode 100644 index 0000000000..0042f8b6dc --- /dev/null +++ b/lib/symfony/form/composer.json @@ -0,0 +1,64 @@ +{ + "name": "symfony/form", + "type": "library", + "description": "Allows to easily create, process and reuse HTML forms", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/validator": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.2|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", + "symfony/error-handler": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.3" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Form\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/lib/symfony/options-resolver/CHANGELOG.md b/lib/symfony/options-resolver/CHANGELOG.md new file mode 100644 index 0000000000..f4de6d01fc --- /dev/null +++ b/lib/symfony/options-resolver/CHANGELOG.md @@ -0,0 +1,96 @@ +CHANGELOG +========= + +6.4 +--- + +* Improve message with full path on invalid type in nested option + +6.3 +--- + + * Add `OptionsResolver::setIgnoreUndefined()` and `OptionConfigurator::ignoreUndefined()` to ignore not defined options while resolving + +6.0 +--- + + * Remove `OptionsResolverIntrospector::getDeprecationMessage()` + +5.3 +--- + + * Add prototype definition for nested options + +5.1.0 +----- + + * added fluent configuration of options using `OptionResolver::define()` + * added `setInfo()` and `getInfo()` methods + * updated the signature of method `OptionsResolver::setDeprecated()` to `OptionsResolver::setDeprecation(string $option, string $package, string $version, $message)` + * deprecated `OptionsResolverIntrospector::getDeprecationMessage()`, use `OptionsResolverIntrospector::getDeprecation()` instead + +5.0.0 +----- + + * added argument `$triggerDeprecation` to `OptionsResolver::offsetGet()` + +4.3.0 +----- + + * added `OptionsResolver::addNormalizer` method + +4.2.0 +----- + + * added support for nested options definition + * added `setDeprecated` and `isDeprecated` methods + +3.4.0 +----- + + * added `OptionsResolverIntrospector` to inspect options definitions inside an `OptionsResolver` instance + * added array of types support in allowed types (e.g int[]) + +2.6.0 +----- + + * deprecated OptionsResolverInterface + * [BC BREAK] removed "array" type hint from OptionsResolverInterface methods + setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and + addAllowedTypes() + * added OptionsResolver::setDefault() + * added OptionsResolver::hasDefault() + * added OptionsResolver::setNormalizer() + * added OptionsResolver::isRequired() + * added OptionsResolver::getRequiredOptions() + * added OptionsResolver::isMissing() + * added OptionsResolver::getMissingOptions() + * added OptionsResolver::setDefined() + * added OptionsResolver::isDefined() + * added OptionsResolver::getDefinedOptions() + * added OptionsResolver::remove() + * added OptionsResolver::clear() + * deprecated OptionsResolver::replaceDefaults() + * deprecated OptionsResolver::setOptional() in favor of setDefined() + * deprecated OptionsResolver::isKnown() in favor of isDefined() + * [BC BREAK] OptionsResolver::isRequired() returns true now if a required + option has a default value set + * [BC BREAK] merged Options into OptionsResolver and turned Options into an + interface + * deprecated Options::overload() (now in OptionsResolver) + * deprecated Options::set() (now in OptionsResolver) + * deprecated Options::get() (now in OptionsResolver) + * deprecated Options::has() (now in OptionsResolver) + * deprecated Options::replace() (now in OptionsResolver) + * [BC BREAK] Options::get() (now in OptionsResolver) can only be used within + lazy option/normalizer closures now + * [BC BREAK] removed Traversable interface from Options since using within + lazy option/normalizer closures resulted in exceptions + * [BC BREAK] removed Options::all() since using within lazy option/normalizer + closures resulted in exceptions + * [BC BREAK] OptionDefinitionException now extends LogicException instead of + RuntimeException + * [BC BREAK] normalizers are not executed anymore for unset options + * normalizers are executed after validating the options now + * [BC BREAK] an UndefinedOptionsException is now thrown instead of an + InvalidOptionsException when non-existing options are passed diff --git a/lib/symfony/options-resolver/Debug/OptionsResolverIntrospector.php b/lib/symfony/options-resolver/Debug/OptionsResolverIntrospector.php new file mode 100644 index 0000000000..dab741b426 --- /dev/null +++ b/lib/symfony/options-resolver/Debug/OptionsResolverIntrospector.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Debug; + +use Symfony\Component\OptionsResolver\Exception\NoConfigurationException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Maxime Steinhausser + * + * @final + */ +class OptionsResolverIntrospector +{ + private \Closure $get; + + public function __construct(OptionsResolver $optionsResolver) + { + $this->get = \Closure::bind(function ($property, $option, $message) { + /** @var OptionsResolver $this */ + if (!$this->isDefined($option)) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist.', $option)); + } + + if (!\array_key_exists($option, $this->{$property})) { + throw new NoConfigurationException($message); + } + + return $this->{$property}[$option]; + }, $optionsResolver, $optionsResolver); + } + + /** + * @throws NoConfigurationException on no configured value + */ + public function getDefault(string $option): mixed + { + return ($this->get)('defaults', $option, \sprintf('No default value was set for the "%s" option.', $option)); + } + + /** + * @return \Closure[] + * + * @throws NoConfigurationException on no configured closures + */ + public function getLazyClosures(string $option): array + { + return ($this->get)('lazy', $option, \sprintf('No lazy closures were set for the "%s" option.', $option)); + } + + /** + * @return string[] + * + * @throws NoConfigurationException on no configured types + */ + public function getAllowedTypes(string $option): array + { + return ($this->get)('allowedTypes', $option, \sprintf('No allowed types were set for the "%s" option.', $option)); + } + + /** + * @return mixed[] + * + * @throws NoConfigurationException on no configured values + */ + public function getAllowedValues(string $option): array + { + return ($this->get)('allowedValues', $option, \sprintf('No allowed values were set for the "%s" option.', $option)); + } + + /** + * @throws NoConfigurationException on no configured normalizer + */ + public function getNormalizer(string $option): \Closure + { + return current($this->getNormalizers($option)); + } + + /** + * @throws NoConfigurationException when no normalizer is configured + */ + public function getNormalizers(string $option): array + { + return ($this->get)('normalizers', $option, \sprintf('No normalizer was set for the "%s" option.', $option)); + } + + /** + * @throws NoConfigurationException on no configured deprecation + */ + public function getDeprecation(string $option): array + { + return ($this->get)('deprecated', $option, \sprintf('No deprecation was set for the "%s" option.', $option)); + } +} diff --git a/lib/symfony/options-resolver/Exception/AccessException.php b/lib/symfony/options-resolver/Exception/AccessException.php new file mode 100644 index 0000000000..c12b680645 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/AccessException.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\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option outside of or write it inside of + * {@link \Symfony\Component\OptionsResolver\Options::resolve()}. + * + * @author Bernhard Schussek + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/lib/symfony/options-resolver/Exception/ExceptionInterface.php b/lib/symfony/options-resolver/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..ea99d050e4 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Marker interface for all exceptions thrown by the OptionsResolver component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/lib/symfony/options-resolver/Exception/InvalidArgumentException.php b/lib/symfony/options-resolver/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..6d421d68b3 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when an argument is invalid. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/lib/symfony/options-resolver/Exception/InvalidOptionsException.php b/lib/symfony/options-resolver/Exception/InvalidOptionsException.php new file mode 100644 index 0000000000..6fd4f125f4 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/InvalidOptionsException.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\OptionsResolver\Exception; + +/** + * Thrown when the value of an option does not match its validation rules. + * + * You should make sure a valid value is passed to the option. + * + * @author Bernhard Schussek + */ +class InvalidOptionsException extends InvalidArgumentException +{ +} diff --git a/lib/symfony/options-resolver/Exception/MissingOptionsException.php b/lib/symfony/options-resolver/Exception/MissingOptionsException.php new file mode 100644 index 0000000000..faa487f16f --- /dev/null +++ b/lib/symfony/options-resolver/Exception/MissingOptionsException.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\OptionsResolver\Exception; + +/** + * Exception thrown when a required option is missing. + * + * Add the option to the passed options array. + * + * @author Bernhard Schussek + */ +class MissingOptionsException extends InvalidArgumentException +{ +} diff --git a/lib/symfony/options-resolver/Exception/NoConfigurationException.php b/lib/symfony/options-resolver/Exception/NoConfigurationException.php new file mode 100644 index 0000000000..6693ec14df --- /dev/null +++ b/lib/symfony/options-resolver/Exception/NoConfigurationException.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\OptionsResolver\Exception; + +use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; + +/** + * Thrown when trying to introspect an option definition property + * for which no value was configured inside the OptionsResolver instance. + * + * @see OptionsResolverIntrospector + * + * @author Maxime Steinhausser + */ +class NoConfigurationException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/lib/symfony/options-resolver/Exception/NoSuchOptionException.php b/lib/symfony/options-resolver/Exception/NoSuchOptionException.php new file mode 100644 index 0000000000..4c3280f4c7 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/NoSuchOptionException.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\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option that has no value set. + * + * When accessing optional options from within a lazy option or normalizer you should first + * check whether the optional option is set. You can do this with `isset($options['optional'])`. + * In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can + * occur when evaluating lazy options. + * + * @author Tobias Schultze + */ +class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/lib/symfony/options-resolver/Exception/OptionDefinitionException.php b/lib/symfony/options-resolver/Exception/OptionDefinitionException.php new file mode 100644 index 0000000000..e8e339d446 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/OptionDefinitionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when two lazy options have a cyclic dependency. + * + * @author Bernhard Schussek + */ +class OptionDefinitionException extends \LogicException implements ExceptionInterface +{ +} diff --git a/lib/symfony/options-resolver/Exception/UndefinedOptionsException.php b/lib/symfony/options-resolver/Exception/UndefinedOptionsException.php new file mode 100644 index 0000000000..6ca3fce470 --- /dev/null +++ b/lib/symfony/options-resolver/Exception/UndefinedOptionsException.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\OptionsResolver\Exception; + +/** + * Exception thrown when an undefined option is passed. + * + * You should remove the options in question from your code or define them + * beforehand. + * + * @author Bernhard Schussek + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/lib/symfony/options-resolver/LICENSE b/lib/symfony/options-resolver/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/options-resolver/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/options-resolver/OptionConfigurator.php b/lib/symfony/options-resolver/OptionConfigurator.php new file mode 100644 index 0000000000..3aa37288ae --- /dev/null +++ b/lib/symfony/options-resolver/OptionConfigurator.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; + +final class OptionConfigurator +{ + private string $name; + private OptionsResolver $resolver; + + public function __construct(string $name, OptionsResolver $resolver) + { + $this->name = $name; + $this->resolver = $resolver; + $this->resolver->setDefined($name); + } + + /** + * Adds allowed types for this option. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function allowedTypes(string ...$types): static + { + $this->resolver->setAllowedTypes($this->name, $types); + + return $this; + } + + /** + * Sets allowed values for this option. + * + * @param mixed ...$values One or more acceptable values/closures + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function allowedValues(mixed ...$values): static + { + $this->resolver->setAllowedValues($this->name, $values); + + return $this; + } + + /** + * Sets the default value for this option. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function default(mixed $value): static + { + $this->resolver->setDefault($this->name, $value); + + return $this; + } + + /** + * Defines an option configurator with the given name. + */ + public function define(string $option): self + { + return $this->resolver->define($option); + } + + /** + * Marks this option as deprecated. + * + * @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|\Closure $message The deprecation message to use + * + * @return $this + */ + public function deprecated(string $package, string $version, string|\Closure $message = 'The option "%name%" is deprecated.'): static + { + $this->resolver->setDeprecated($this->name, $package, $version, $message); + + return $this; + } + + /** + * Sets the normalizer for this option. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function normalize(\Closure $normalizer): static + { + $this->resolver->setNormalizer($this->name, $normalizer); + + return $this; + } + + /** + * Marks this option as required. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function required(): static + { + $this->resolver->setRequired($this->name); + + return $this; + } + + /** + * Sets an info message for an option. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function info(string $info): static + { + $this->resolver->setInfo($this->name, $info); + + return $this; + } + + /** + * Sets whether ignore undefined options. + * + * @return $this + */ + public function ignoreUndefined(bool $ignore = true): static + { + $this->resolver->setIgnoreUndefined($ignore); + + return $this; + } +} diff --git a/lib/symfony/options-resolver/Options.php b/lib/symfony/options-resolver/Options.php new file mode 100644 index 0000000000..d444ec4230 --- /dev/null +++ b/lib/symfony/options-resolver/Options.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\OptionsResolver; + +/** + * Contains resolved option values. + * + * @author Bernhard Schussek + * @author Tobias Schultze + */ +interface Options extends \ArrayAccess, \Countable +{ +} diff --git a/lib/symfony/options-resolver/OptionsResolver.php b/lib/symfony/options-resolver/OptionsResolver.php new file mode 100644 index 0000000000..da7553cfb2 --- /dev/null +++ b/lib/symfony/options-resolver/OptionsResolver.php @@ -0,0 +1,1317 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; + +/** + * Validates options and merges them with default values. + * + * @author Bernhard Schussek + * @author Tobias Schultze + */ +class OptionsResolver implements Options +{ + private const VALIDATION_FUNCTIONS = [ + 'bool' => 'is_bool', + 'boolean' => 'is_bool', + 'int' => 'is_int', + 'integer' => 'is_int', + 'long' => 'is_int', + 'float' => 'is_float', + 'double' => 'is_float', + 'real' => 'is_float', + 'numeric' => 'is_numeric', + 'string' => 'is_string', + 'scalar' => 'is_scalar', + 'array' => 'is_array', + 'iterable' => 'is_iterable', + 'countable' => 'is_countable', + 'callable' => 'is_callable', + 'object' => 'is_object', + 'resource' => 'is_resource', + ]; + + /** + * The names of all defined options. + */ + private array $defined = []; + + /** + * The default option values. + */ + private array $defaults = []; + + /** + * A list of closure for nested options. + * + * @var \Closure[][] + */ + private array $nested = []; + + /** + * The names of required options. + */ + private array $required = []; + + /** + * The resolved option values. + */ + private array $resolved = []; + + /** + * A list of normalizer closures. + * + * @var \Closure[][] + */ + private array $normalizers = []; + + /** + * A list of accepted values for each option. + */ + private array $allowedValues = []; + + /** + * A list of accepted types for each option. + */ + private array $allowedTypes = []; + + /** + * A list of info messages for each option. + */ + private array $info = []; + + /** + * A list of closures for evaluating lazy options. + */ + private array $lazy = []; + + /** + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + */ + private array $calling = []; + + /** + * A list of deprecated options. + */ + private array $deprecated = []; + + /** + * The list of options provided by the user. + */ + private array $given = []; + + /** + * Whether the instance is locked for reading. + * + * Once locked, the options cannot be changed anymore. This is + * necessary in order to avoid inconsistencies during the resolving + * process. If any option is changed after being read, all evaluated + * lazy options that depend on this option would become invalid. + */ + private bool $locked = false; + + private array $parentsOptions = []; + + /** + * Whether the whole options definition is marked as array prototype. + */ + private ?bool $prototype = null; + + /** + * The prototype array's index that is being read. + */ + private int|string|null $prototypeIndex = null; + + /** + * Whether to ignore undefined options. + */ + private bool $ignoreUndefined = false; + + /** + * Sets the default value of a given option. + * + * If the default value should be set based on other options, you can pass + * a closure with the following signature: + * + * function (Options $options) { + * // ... + * } + * + * The closure will be evaluated when {@link resolve()} is called. The + * closure has access to the resolved values of other options through the + * passed {@link Options} instance: + * + * function (Options $options) { + * if (isset($options['port'])) { + * // ... + * } + * } + * + * If you want to access the previously set default value, add a second + * argument to the closure's signature: + * + * $options->setDefault('name', 'Default Name'); + * + * $options->setDefault('name', function (Options $options, $previousValue) { + * // 'Default Name' === $previousValue + * }); + * + * This is mostly useful if the configuration of the {@link Options} object + * is spread across different locations of your code, such as base and + * sub-classes. + * + * If you want to define nested options, you can pass a closure with the + * following signature: + * + * $options->setDefault('database', function (OptionsResolver $resolver) { + * $resolver->setDefined(['dbname', 'host', 'port', 'user', 'pass']); + * } + * + * To get access to the parent options, add a second argument to the closure's + * signature: + * + * function (OptionsResolver $resolver, Options $parent) { + * // 'default' === $parent['connection'] + * } + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefault(string $option, mixed $value): static + { + // Setting is not possible once resolving starts, because then lazy + // options could manipulate the state of the object, leading to + // inconsistent results. + if ($this->locked) { + throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); + } + + // If an option is a closure that should be evaluated lazily, store it + // in the "lazy" property. + if ($value instanceof \Closure) { + $reflClosure = new \ReflectionFunction($value); + $params = $reflClosure->getParameters(); + + if (isset($params[0]) && Options::class === $this->getParameterClassName($params[0])) { + // Initialize the option if no previous value exists + if (!isset($this->defaults[$option])) { + $this->defaults[$option] = null; + } + + // Ignore previous lazy options if the closure has no second parameter + if (!isset($this->lazy[$option]) || !isset($params[1])) { + $this->lazy[$option] = []; + } + + // Store closure for later evaluation + $this->lazy[$option][] = $value; + $this->defined[$option] = true; + + // Make sure the option is processed and is not nested anymore + unset($this->resolved[$option], $this->nested[$option]); + + return $this; + } + + if (isset($params[0]) && ($type = $params[0]->getType()) instanceof \ReflectionNamedType && self::class === $type->getName() && (!isset($params[1]) || (($type = $params[1]->getType()) instanceof \ReflectionNamedType && Options::class === $type->getName()))) { + // Store closure for later evaluation + $this->nested[$option][] = $value; + $this->defaults[$option] = []; + $this->defined[$option] = true; + + // Make sure the option is processed and is not lazy anymore + unset($this->resolved[$option], $this->lazy[$option]); + + return $this; + } + } + + // This option is not lazy nor nested anymore + unset($this->lazy[$option], $this->nested[$option]); + + // Yet undefined options can be marked as resolved, because we only need + // to resolve options with lazy closures, normalizers or validation + // rules, none of which can exist for undefined options + // If the option was resolved before, update the resolved value + if (!isset($this->defined[$option]) || \array_key_exists($option, $this->resolved)) { + $this->resolved[$option] = $value; + } + + $this->defaults[$option] = $value; + $this->defined[$option] = true; + + return $this; + } + + /** + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefaults(array $defaults): static + { + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); + } + + return $this; + } + + /** + * Returns whether a default value is set for an option. + * + * Returns true if {@link setDefault()} was called for this option. + * An option is also considered set if it was set to null. + */ + public function hasDefault(string $option): bool + { + return \array_key_exists($option, $this->defaults); + } + + /** + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setRequired(string|array $optionNames): static + { + if ($this->locked) { + throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + $this->required[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is required. + * + * An option is required if it was passed to {@link setRequired()}. + */ + public function isRequired(string $option): bool + { + return isset($this->required[$option]); + } + + /** + * Returns the names of all required options. + * + * @return string[] + * + * @see isRequired() + */ + public function getRequiredOptions(): array + { + return array_keys($this->required); + } + + /** + * Returns whether an option is missing a default value. + * + * An option is missing if it was passed to {@link setRequired()}, but not + * to {@link setDefault()}. This option must be passed explicitly to + * {@link resolve()}, otherwise an exception will be thrown. + */ + public function isMissing(string $option): bool + { + return isset($this->required[$option]) && !\array_key_exists($option, $this->defaults); + } + + /** + * Returns the names of all options missing a default value. + * + * @return string[] + */ + public function getMissingOptions(): array + { + return array_keys(array_diff_key($this->required, $this->defaults)); + } + + /** + * Defines a valid option name. + * + * Defines an option name without setting a default value. The option will + * be accepted when passed to {@link resolve()}. When not passed, the + * option will not be included in the resolved options. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefined(string|array $optionNames): static + { + if ($this->locked) { + throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is defined. + * + * Returns true for any option passed to {@link setDefault()}, + * {@link setRequired()} or {@link setDefined()}. + */ + public function isDefined(string $option): bool + { + return isset($this->defined[$option]); + } + + /** + * Returns the names of all defined options. + * + * @return string[] + * + * @see isDefined() + */ + public function getDefinedOptions(): array + { + return array_keys($this->defined); + } + + public function isNested(string $option): bool + { + return isset($this->nested[$option]); + } + + /** + * Deprecates an option, allowed types or values. + * + * Instead of passing the message, you may also pass a closure with the + * following signature: + * + * function (Options $options, $value): string { + * // ... + * } + * + * The closure receives the value as argument and should return a string. + * Return an empty string to ignore the option deprecation. + * + * The closure is invoked when {@link resolve()} is called. The parameter + * passed to the closure is the value of the option after validating it + * and before normalizing it. + * + * @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|\Closure $message The deprecation message to use + * + * @return $this + */ + public function setDeprecated(string $option, string $package, string $version, string|\Closure $message = 'The option "%name%" is deprecated.'): static + { + if ($this->locked) { + throw new AccessException('Options cannot be deprecated from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist, defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if (!\is_string($message) && !$message instanceof \Closure) { + throw new InvalidArgumentException(\sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', get_debug_type($message))); + } + + // ignore if empty string + if ('' === $message) { + return $this; + } + + $this->deprecated[$option] = [ + 'package' => $package, + 'version' => $version, + 'message' => $message, + ]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + public function isDeprecated(string $option): bool + { + return isset($this->deprecated[$option]); + } + + /** + * Sets the normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * function (Options $options, $value) { + * // ... + * } + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setNormalizer(string $option, \Closure $normalizer) + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->normalizers[$option] = [$normalizer]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds a normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * function (Options $options, $value): mixed { + * // ... + * } + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addNormalizer(string $option, \Closure $normalizer, bool $forcePrepend = false): static + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if ($forcePrepend) { + $this->normalizers[$option] ??= []; + array_unshift($this->normalizers[$option], $normalizer); + } else { + $this->normalizers[$option][] = $normalizer; + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed values for an option. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedValues(string $option, mixed $allowedValues) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : [$allowedValues]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed values for an option. + * + * The values are merged with the allowed values defined previously. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedValues(string $option, mixed $allowedValues) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if (!\is_array($allowedValues)) { + $allowedValues = [$allowedValues]; + } + + if (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed types for an option. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string|string[] $allowedTypes One or more accepted types + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedTypes(string $option, string|array $allowedTypes) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->allowedTypes[$option] = (array) $allowedTypes; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed types for an option. + * + * The types are merged with the allowed types defined previously. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string|string[] $allowedTypes One or more accepted types + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedTypes(string $option, string|array $allowedTypes) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if (!isset($this->allowedTypes[$option])) { + $this->allowedTypes[$option] = (array) $allowedTypes; + } else { + $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Defines an option configurator with the given name. + */ + public function define(string $option): OptionConfigurator + { + if (isset($this->defined[$option])) { + throw new OptionDefinitionException(\sprintf('The option "%s" is already defined.', $option)); + } + + return new OptionConfigurator($option, $this); + } + + /** + * Sets an info message for an option. + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setInfo(string $option, string $info): static + { + if ($this->locked) { + throw new AccessException('The Info message cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->info[$option] = $info; + + return $this; + } + + /** + * Gets the info message for an option. + */ + public function getInfo(string $option): ?string + { + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + return $this->info[$option] ?? null; + } + + /** + * Marks the whole options definition as array prototype. + * + * @return $this + * + * @throws AccessException If called from a lazy option, a normalizer or a root definition + */ + public function setPrototype(bool $prototype): static + { + if ($this->locked) { + throw new AccessException('The prototype property cannot be set from a lazy option or normalizer.'); + } + + if (null === $this->prototype && $prototype) { + throw new AccessException('The prototype property cannot be set from a root definition.'); + } + + $this->prototype = $prototype; + + return $this; + } + + public function isPrototype(): bool + { + return $this->prototype ?? false; + } + + /** + * Removes the option with the given name. + * + * Undefined options are ignored. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function remove(string|array $optionNames): static + { + if ($this->locked) { + throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option], $this->info[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function clear(): static + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); + } + + $this->defined = []; + $this->defaults = []; + $this->nested = []; + $this->required = []; + $this->resolved = []; + $this->lazy = []; + $this->normalizers = []; + $this->allowedTypes = []; + $this->allowedValues = []; + $this->deprecated = []; + $this->info = []; + + return $this; + } + + /** + * Merges options with the default values stored in the container and + * validates them. + * + * Exceptions are thrown if: + * + * - Undefined options are passed; + * - Required options are missing; + * - Options have invalid types; + * - Options have invalid values. + * + * @throws UndefinedOptionsException If an option name is undefined + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws MissingOptionsException If a required option is missing + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + * @throws NoSuchOptionException If a lazy option reads an unavailable option + * @throws AccessException If called from a lazy option or normalizer + */ + public function resolve(array $options = []): array + { + if ($this->locked) { + throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); + } + + // Allow this method to be called multiple times + $clone = clone $this; + + // Make sure that no unknown options are passed + $diff = $this->ignoreUndefined ? [] : array_diff_key($options, $clone->defined); + + if (\count($diff) > 0) { + ksort($clone->defined); + ksort($diff); + + throw new UndefinedOptionsException(\sprintf((\count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', $this->formatOptions(array_keys($diff)), implode('", "', array_keys($clone->defined)))); + } + + // Override options set by the user + foreach ($options as $option => $value) { + if ($this->ignoreUndefined && !isset($clone->defined[$option])) { + continue; + } + + $clone->given[$option] = true; + $clone->defaults[$option] = $value; + unset($clone->resolved[$option], $clone->lazy[$option]); + } + + // Check whether any required option is missing + $diff = array_diff_key($clone->required, $clone->defaults); + + if (\count($diff) > 0) { + ksort($diff); + + throw new MissingOptionsException(\sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', $this->formatOptions(array_keys($diff)))); + } + + // Lock the container + $clone->locked = true; + + // Now process the individual options. Use offsetGet(), which resolves + // the option itself and any options that the option depends on + foreach ($clone->defaults as $option => $_) { + $clone->offsetGet($option); + } + + return $clone->resolved; + } + + /** + * Returns the resolved value of an option. + * + * @param bool $triggerDeprecation Whether to trigger the deprecation or not (true by default) + * + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws NoSuchOptionException If the option is not set + * @throws InvalidOptionsException If the option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + */ + public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + // Shortcut for resolved options + if (isset($this->resolved[$option]) || \array_key_exists($option, $this->resolved)) { + if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || $this->calling) && \is_string($this->deprecated[$option]['message'])) { + trigger_deprecation($this->deprecated[$option]['package'], $this->deprecated[$option]['version'], strtr($this->deprecated[$option]['message'], ['%name%' => $option])); + } + + return $this->resolved[$option]; + } + + // Check whether the option is set at all + if (!isset($this->defaults[$option]) && !\array_key_exists($option, $this->defaults)) { + if (!isset($this->defined[$option])) { + throw new NoSuchOptionException(\sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + throw new NoSuchOptionException(\sprintf('The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', $this->formatOptions([$option]))); + } + + $value = $this->defaults[$option]; + + // Resolve the option if it is a nested definition + if (isset($this->nested[$option])) { + // If the closure is already being called, we have a cyclic dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + if (!\is_array($value)) { + throw new InvalidOptionsException(\sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $this->formatOptions([$option]), $this->formatValue($value), get_debug_type($value))); + } + + // The following section must be protected from cyclic calls. + $this->calling[$option] = true; + try { + $resolver = new self(); + $resolver->prototype = false; + $resolver->parentsOptions = $this->parentsOptions; + $resolver->parentsOptions[] = $option; + foreach ($this->nested[$option] as $closure) { + $closure($resolver, $this); + } + + if ($resolver->prototype) { + $values = []; + foreach ($value as $index => $prototypeValue) { + if (!\is_array($prototypeValue)) { + throw new InvalidOptionsException(\sprintf('The value of the option "%s" is expected to be of type array of array, but is of type array of "%s".', $this->formatOptions([$option]), get_debug_type($prototypeValue))); + } + + $resolver->prototypeIndex = $index; + $values[$index] = $resolver->resolve($prototypeValue); + } + $value = $values; + } else { + $value = $resolver->resolve($value); + } + } finally { + $resolver->prototypeIndex = null; + unset($this->calling[$option]); + } + } + + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } finally { + unset($this->calling[$option]); + } + // END + } + + // Validate the type of the resolved option + if (isset($this->allowedTypes[$option])) { + $valid = true; + $invalidTypes = []; + + foreach ($this->allowedTypes[$option] as $type) { + if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) { + break; + } + } + + if (!$valid) { + $fmtActualValue = $this->formatValue($value); + $fmtAllowedTypes = implode('" or "', $this->allowedTypes[$option]); + $fmtProvidedTypes = implode('|', array_keys($invalidTypes)); + $allowedContainsArrayType = \count(array_filter($this->allowedTypes[$option], static fn ($item) => str_ends_with($item, '[]'))) > 0; + + if (\is_array($value) && $allowedContainsArrayType) { + throw new InvalidOptionsException(\sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $this->formatOptions([$option]), $fmtActualValue, $fmtAllowedTypes, $fmtProvidedTypes)); + } + + throw new InvalidOptionsException(\sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $this->formatOptions([$option]), $fmtActualValue, $fmtAllowedTypes, $fmtProvidedTypes)); + } + } + + // Validate the value of the resolved option + if (isset($this->allowedValues[$option])) { + $success = false; + $printableAllowedValues = []; + + foreach ($this->allowedValues[$option] as $allowedValue) { + if ($allowedValue instanceof \Closure) { + if ($allowedValue($value)) { + $success = true; + break; + } + + // Don't include closures in the exception message + continue; + } + + if ($value === $allowedValue) { + $success = true; + break; + } + + $printableAllowedValues[] = $allowedValue; + } + + if (!$success) { + $message = \sprintf( + 'The option "%s" with value %s is invalid.', + $this->formatOptions([$option]), + $this->formatValue($value) + ); + + if (\count($printableAllowedValues) > 0) { + $message .= \sprintf( + ' Accepted values are: %s.', + $this->formatValues($printableAllowedValues) + ); + } + + if (isset($this->info[$option])) { + $message .= \sprintf(' Info: %s.', $this->info[$option]); + } + + throw new InvalidOptionsException($message); + } + } + + // Check whether the option is deprecated + // and it is provided by the user or is being called from a lazy evaluation + if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && \is_string($this->deprecated[$option]['message'])))) { + $deprecation = $this->deprecated[$option]; + $message = $this->deprecated[$option]['message']; + + if ($message instanceof \Closure) { + // If the closure is already being called, we have a cyclic dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + $this->calling[$option] = true; + try { + if (!\is_string($message = $message($this, $value))) { + throw new InvalidOptionsException(\sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', get_debug_type($message))); + } + } finally { + unset($this->calling[$option]); + } + } + + if ('' !== $message) { + trigger_deprecation($deprecation['package'], $deprecation['version'], strtr($message, ['%name%' => $option])); + } + } + + // Normalize the validated option + if (isset($this->normalizers[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->normalizers[$option] as $normalizer) { + $value = $normalizer($this, $value); + } + } finally { + unset($this->calling[$option]); + } + // END + } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; + } + + private function verifyTypes(string $type, mixed $value, array &$invalidTypes, int $level = 0): bool + { + if (\is_array($value) && str_ends_with($type, '[]')) { + $type = substr($type, 0, -2); + $valid = true; + + foreach ($value as $val) { + if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) { + $valid = false; + } + } + + return $valid; + } + + if (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) { + return true; + } + + if (!$invalidTypes || $level > 0) { + $invalidTypes[get_debug_type($value)] = true; + } + + return false; + } + + /** + * Returns whether a resolved option with the given name exists. + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \ArrayAccess::offsetExists() + */ + public function offsetExists(mixed $option): bool + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + return \array_key_exists($option, $this->defaults); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet(mixed $option, mixed $value): void + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset(mixed $option): void + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * Returns the number of set options. + * + * This may be only a subset of the defined options. + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \Countable::count() + */ + public function count(): int + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } + + return \count($this->defaults); + } + + /** + * Sets whether ignore undefined options. + * + * @return $this + */ + public function setIgnoreUndefined(bool $ignore = true): static + { + $this->ignoreUndefined = $ignore; + + return $this; + } + + /** + * Returns a string representation of the value. + * + * This method returns the equivalent PHP tokens for most scalar types + * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped + * in double quotes ("). + */ + private function formatValue(mixed $value): string + { + if (\is_object($value)) { + return $value::class; + } + + if (\is_array($value)) { + return 'array'; + } + + if (\is_string($value)) { + return '"'.$value.'"'; + } + + if (\is_resource($value)) { + return 'resource'; + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } + + /** + * Returns a string representation of a list of values. + * + * Each of the values is converted to a string using + * {@link formatValue()}. The values are then concatenated with commas. + * + * @see formatValue() + */ + private function formatValues(array $values): string + { + foreach ($values as $key => $value) { + $values[$key] = $this->formatValue($value); + } + + return implode(', ', $values); + } + + private function formatOptions(array $options): string + { + if ($this->parentsOptions) { + $prefix = array_shift($this->parentsOptions); + if ($this->parentsOptions) { + $prefix .= \sprintf('[%s]', implode('][', $this->parentsOptions)); + } + + if ($this->prototype && null !== $this->prototypeIndex) { + $prefix .= \sprintf('[%s]', $this->prototypeIndex); + } + + $options = array_map(static fn (string $option): string => \sprintf('%s[%s]', $prefix, $option), $options); + } + + return implode('", "', $options); + } + + private function getParameterClassName(\ReflectionParameter $parameter): ?string + { + if (!($type = $parameter->getType()) instanceof \ReflectionNamedType || $type->isBuiltin()) { + return null; + } + + return $type->getName(); + } +} diff --git a/lib/symfony/options-resolver/README.md b/lib/symfony/options-resolver/README.md new file mode 100644 index 0000000000..c63b9005eb --- /dev/null +++ b/lib/symfony/options-resolver/README.md @@ -0,0 +1,15 @@ +OptionsResolver Component +========================= + +The OptionsResolver component is `array_replace` on steroids. It allows you to +create an options system with required options, defaults, validation (type, +value), normalization and more. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/options_resolver.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/lib/symfony/options-resolver/composer.json b/lib/symfony/options-resolver/composer.json new file mode 100644 index 0000000000..9f2daf4e7b --- /dev/null +++ b/lib/symfony/options-resolver/composer.json @@ -0,0 +1,29 @@ +{ + "name": "symfony/options-resolver", + "type": "library", + "description": "Provides an improved replacement for the array_replace PHP function", + "keywords": ["options", "config", "configuration"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/lib/symfony/polyfill-intl-icu/Collator.php b/lib/symfony/polyfill-intl-icu/Collator.php new file mode 100644 index 0000000000..2f952cdf53 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Collator.php @@ -0,0 +1,262 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu; + +use Symfony\Polyfill\Intl\Icu\Exception\MethodArgumentValueNotImplementedException; +use Symfony\Polyfill\Intl\Icu\Exception\MethodNotImplementedException; + +/** + * Replacement for PHP's native {@link \Collator} class. + * + * The only methods currently supported in this class are: + * + * - {@link \__construct} + * - {@link create} + * - {@link asort} + * - {@link getErrorCode} + * - {@link getErrorMessage} + * - {@link getLocale} + * + * @author Igor Wiedler + * @author Bernhard Schussek + * + * @internal + */ +abstract class Collator +{ + /* Attribute constants */ + public const FRENCH_COLLATION = 0; + public const ALTERNATE_HANDLING = 1; + public const CASE_FIRST = 2; + public const CASE_LEVEL = 3; + public const NORMALIZATION_MODE = 4; + public const STRENGTH = 5; + public const HIRAGANA_QUATERNARY_MODE = 6; + public const NUMERIC_COLLATION = 7; + + /* Attribute constants values */ + public const DEFAULT_VALUE = -1; + + public const PRIMARY = 0; + public const SECONDARY = 1; + public const TERTIARY = 2; + public const DEFAULT_STRENGTH = 2; + public const QUATERNARY = 3; + public const IDENTICAL = 15; + + public const OFF = 16; + public const ON = 17; + + public const SHIFTED = 20; + public const NON_IGNORABLE = 21; + + public const LOWER_FIRST = 24; + public const UPPER_FIRST = 25; + + /* Sorting options */ + public const SORT_REGULAR = 0; + public const SORT_NUMERIC = 2; + public const SORT_STRING = 1; + + /** + * @param string|null $locale The locale code. The only currently supported locale is "en" (or null using the default locale, i.e. "en") + * + * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed + */ + public function __construct(?string $locale) + { + if ('en' !== $locale && null !== $locale) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported'); + } + } + + /** + * Static constructor. + * + * @param string|null $locale The locale code. The only currently supported locale is "en" (or null using the default locale, i.e. "en") + * + * @return static + * + * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed + */ + public static function create(?string $locale) + { + return new static($locale); + } + + /** + * Sort array maintaining index association. + * + * @param array &$array Input array + * @param int $flags Flags for sorting, can be one of the following: + * Collator::SORT_REGULAR - compare items normally (don't change types) + * Collator::SORT_NUMERIC - compare items numerically + * Collator::SORT_STRING - compare items as strings + * + * @return bool True on success or false on failure + */ + public function asort(array &$array, int $flags = self::SORT_REGULAR) + { + $intlToPlainFlagMap = [ + self::SORT_REGULAR => \SORT_REGULAR, + self::SORT_NUMERIC => \SORT_NUMERIC, + self::SORT_STRING => \SORT_STRING, + ]; + + $plainSortFlag = $intlToPlainFlagMap[$flags] ?? self::SORT_REGULAR; + + return asort($array, $plainSortFlag); + } + + /** + * Not supported. Compare two Unicode strings. + * + * @return int|false + * + * @see https://php.net/collator.compare + * + * @throws MethodNotImplementedException + */ + public function compare(string $string1, string $string2) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Get a value of an integer collator attribute. + * + * @return int|false The attribute value on success or false on error + * + * @see https://php.net/collator.getattribute + * + * @throws MethodNotImplementedException + */ + public function getAttribute(int $attribute) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns collator's last error code. Always returns the U_ZERO_ERROR class constant value. + * + * @return int|false The error code from last collator call + */ + public function getErrorCode() + { + return Icu::U_ZERO_ERROR; + } + + /** + * Returns collator's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value. + * + * @return string|false The error message from last collator call + */ + public function getErrorMessage() + { + return 'U_ZERO_ERROR'; + } + + /** + * Returns the collator's locale. + * + * @return string|false The locale used to create the collator. Currently + * always returns "en". + */ + public function getLocale(int $type = Locale::ACTUAL_LOCALE) + { + return 'en'; + } + + /** + * Not supported. Get sorting key for a string. + * + * @return string|false The collation key for $string + * + * @see https://php.net/collator.getsortkey + * + * @throws MethodNotImplementedException + */ + public function getSortKey(string $string) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Get current collator's strength. + * + * @return int The current collator's strength or false on failure + * + * @see https://php.net/collator.getstrength + * + * @throws MethodNotImplementedException + */ + public function getStrength() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Set a collator's attribute. + * + * @return bool True on success or false on failure + * + * @see https://php.net/collator.setattribute + * + * @throws MethodNotImplementedException + */ + public function setAttribute(int $attribute, int $value) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Set the collator's strength. + * + * @return bool True on success or false on failure + * + * @see https://php.net/collator.setstrength + * + * @throws MethodNotImplementedException + */ + public function setStrength(int $strength) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Sort array using specified collator and sort keys. + * + * @return bool True on success or false on failure + * + * @see https://php.net/collator.sortwithsortkeys + * + * @throws MethodNotImplementedException + */ + public function sortWithSortKeys(array &$array) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Sort array using specified collator. + * + * @return bool True on success or false on failure + * + * @see https://php.net/collator.sort + * + * @throws MethodNotImplementedException + */ + public function sort(array &$array, int $flags = self::SORT_REGULAR) + { + throw new MethodNotImplementedException(__METHOD__); + } +} diff --git a/lib/symfony/polyfill-intl-icu/Currencies.php b/lib/symfony/polyfill-intl-icu/Currencies.php new file mode 100644 index 0000000000..90b1efa690 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Currencies.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\Polyfill\Intl\Icu; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Currencies +{ + private static $data; + + public static function getSymbol(string $currency): ?string + { + $data = self::$data ?? self::$data = require __DIR__.'/Resources/currencies.php'; + + return $data[$currency][0] ?? $data[strtoupper($currency)][0] ?? null; + } + + public static function getFractionDigits(string $currency): int + { + $data = self::$data ?? self::$data = require __DIR__.'/Resources/currencies.php'; + + return $data[$currency][1] ?? $data[strtoupper($currency)][1] ?? $data['DEFAULT'][1]; + } + + public static function getRoundingIncrement(string $currency): int + { + $data = self::$data ?? self::$data = require __DIR__.'/Resources/currencies.php'; + + return $data[$currency][2] ?? $data[strtoupper($currency)][2] ?? $data['DEFAULT'][2]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/AmPmTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/AmPmTransformer.php new file mode 100644 index 0000000000..196c604bed --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/AmPmTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for AM/PM markers format. + * + * @author Igor Wiedler + * + * @internal + */ +class AmPmTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + return $dateTime->format('A'); + } + + public function getReverseMatchingRegExp(int $length): string + { + return 'AM|PM'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'marker' => $matched, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/DayOfWeekTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/DayOfWeekTransformer.php new file mode 100644 index 0000000000..6eedd24443 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/DayOfWeekTransformer.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for day of week format. + * + * @author Igor Wiedler + * + * @internal + */ +class DayOfWeekTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $dayOfWeek = $dateTime->format('l'); + switch ($length) { + case 4: + return $dayOfWeek; + case 5: + return $dayOfWeek[0]; + case 6: + return substr($dayOfWeek, 0, 2); + default: + return substr($dayOfWeek, 0, 3); + } + } + + public function getReverseMatchingRegExp(int $length): string + { + switch ($length) { + case 4: + return 'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday'; + case 5: + return '[MTWFS]'; + case 6: + return 'Mo|Tu|We|Th|Fr|Sa|Su'; + default: + return 'Mon|Tue|Wed|Thu|Fri|Sat|Sun'; + } + } + + public function extractDateOptions(string $matched, int $length): array + { + return []; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/DayOfYearTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/DayOfYearTransformer.php new file mode 100644 index 0000000000..ed78853e01 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/DayOfYearTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for day of year format. + * + * @author Igor Wiedler + * + * @internal + */ +class DayOfYearTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $dayOfYear = (int) $dateTime->format('z') + 1; + + return $this->padLeft($dayOfYear, $length); + } + + public function getReverseMatchingRegExp(int $length): string + { + return '\d{'.$length.'}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return []; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/DayTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/DayTransformer.php new file mode 100644 index 0000000000..bdce79e6e8 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/DayTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for day format. + * + * @author Igor Wiedler + * + * @internal + */ +class DayTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + return $this->padLeft($dateTime->format('j'), $length); + } + + public function getReverseMatchingRegExp(int $length): string + { + return 1 === $length ? '\d{1,2}' : '\d{1,'.$length.'}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'day' => (int) $matched, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php new file mode 100644 index 0000000000..02d071da57 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\DateFormat; + +use Symfony\Polyfill\Intl\Icu\Exception\NotImplementedException; +use Symfony\Polyfill\Intl\Icu\Icu; + +/** + * Parser and formatter for date formats. + * + * @author Igor Wiedler + * + * @internal + */ +class FullTransformer +{ + private $quoteMatch = "'(?:[^']+|'')*'"; + private $implementedChars = 'MLydQqhDEaHkKmsz'; + private $notImplementedChars = 'GYuwWFgecSAZvVW'; + private $regExp; + + /** + * @var Transformer[] + */ + private $transformers; + + private $pattern; + private $timezone; + + /** + * @param string $pattern The pattern to be used to format and/or parse values + * @param string $timezone The timezone to perform the date/time calculations + */ + public function __construct(string $pattern, string $timezone) + { + $this->pattern = $pattern; + $this->timezone = $timezone; + + $implementedCharsMatch = $this->buildCharsMatch($this->implementedChars); + $notImplementedCharsMatch = $this->buildCharsMatch($this->notImplementedChars); + $this->regExp = "/($this->quoteMatch|$implementedCharsMatch|$notImplementedCharsMatch)/"; + + $this->transformers = [ + 'M' => new MonthTransformer(), + 'L' => new MonthTransformer(), + 'y' => new YearTransformer(), + 'd' => new DayTransformer(), + 'q' => new QuarterTransformer(), + 'Q' => new QuarterTransformer(), + 'h' => new Hour1201Transformer(), + 'D' => new DayOfYearTransformer(), + 'E' => new DayOfWeekTransformer(), + 'a' => new AmPmTransformer(), + 'H' => new Hour2400Transformer(), + 'K' => new Hour1200Transformer(), + 'k' => new Hour2401Transformer(), + 'm' => new MinuteTransformer(), + 's' => new SecondTransformer(), + 'z' => new TimezoneTransformer(), + ]; + } + + /** + * Format a DateTime using ICU dateformat pattern. + * + * @return string The formatted value + */ + public function format(\DateTime $dateTime): string + { + $formatted = preg_replace_callback($this->regExp, function ($matches) use ($dateTime) { + return $this->formatReplace($matches[0], $dateTime); + }, $this->pattern); + + return $formatted; + } + + /** + * Return the formatted ICU value for the matched date characters. + * + * @throws NotImplementedException When it encounters a not implemented date character + */ + private function formatReplace(string $dateChars, \DateTime $dateTime): string + { + $length = \strlen($dateChars); + + if ($this->isQuoteMatch($dateChars)) { + return $this->replaceQuoteMatch($dateChars); + } + + if (isset($this->transformers[$dateChars[0]])) { + $transformer = $this->transformers[$dateChars[0]]; + + return $transformer->format($dateTime, $length); + } + + // handle unimplemented characters + if (false !== strpos($this->notImplementedChars, $dateChars[0])) { + throw new NotImplementedException(sprintf('Unimplemented date character "%s" in format "%s".', $dateChars[0], $this->pattern)); + } + + return ''; + } + + /** + * Parse a pattern based string to a timestamp value. + * + * @param \DateTime $dateTime A configured DateTime object to use to perform the date calculation + * @param string $value String to convert to a time value + * + * @return int|false The corresponding Unix timestamp + * + * @throws \InvalidArgumentException When the value can not be matched with pattern + */ + public function parse(\DateTime $dateTime, string $value) + { + $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern); + $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/'; + + $options = []; + + if (preg_match($reverseMatchingRegExp, $value, $matches)) { + $matches = $this->normalizeArray($matches); + + foreach ($this->transformers as $char => $transformer) { + if (isset($matches[$char])) { + $length = \strlen($matches[$char]['pattern']); + $options = array_merge($options, $transformer->extractDateOptions($matches[$char]['value'], $length)); + } + } + + // reset error code and message + Icu::setError(Icu::U_ZERO_ERROR); + + return $this->calculateUnixTimestamp($dateTime, $options); + } + + // behave like the intl extension + Icu::setError(Icu::U_PARSE_ERROR, 'Date parsing failed'); + + return false; + } + + /** + * Retrieve a regular expression to match with a formatted value. + * + * @return string The reverse matching regular expression with named captures being formed by the + * transformer index in the $transformer array + */ + private function getReverseMatchingRegExp(string $pattern): string + { + $escapedPattern = preg_quote($pattern, '/'); + + // ICU 4.8 recognizes slash ("/") in a value to be parsed as a dash ("-") and vice-versa + // when parsing a date/time value + $escapedPattern = preg_replace('/\\\[\-|\/]/', '[\/\-]', $escapedPattern); + + $reverseMatchingRegExp = preg_replace_callback($this->regExp, function ($matches) { + $length = \strlen($matches[0]); + $transformerIndex = $matches[0][0]; + + $dateChars = $matches[0]; + if ($this->isQuoteMatch($dateChars)) { + return $this->replaceQuoteMatch($dateChars); + } + + if (isset($this->transformers[$transformerIndex])) { + $transformer = $this->transformers[$transformerIndex]; + $captureName = str_repeat($transformerIndex, $length); + + return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')'; + } + + return null; + }, $escapedPattern); + + return $reverseMatchingRegExp; + } + + /** + * Check if the first char of a string is a single quote. + */ + private function isQuoteMatch(string $quoteMatch): bool + { + return "'" === $quoteMatch[0]; + } + + /** + * Replaces single quotes at the start or end of a string with two single quotes. + */ + private function replaceQuoteMatch(string $quoteMatch): string + { + if (preg_match("/^'+$/", $quoteMatch)) { + return str_replace("''", "'", $quoteMatch); + } + + return str_replace("''", "'", substr($quoteMatch, 1, -1)); + } + + /** + * Builds a chars match regular expression. + */ + private function buildCharsMatch(string $specialChars): string + { + $specialCharsArray = str_split($specialChars); + + $specialCharsMatch = implode('|', array_map(function ($char) { + return $char.'+'; + }, $specialCharsArray)); + + return $specialCharsMatch; + } + + /** + * Normalize a preg_replace match array, removing the numeric keys and returning an associative array + * with the value and pattern values for the matched Transformer. + */ + private function normalizeArray(array $data): array + { + $ret = []; + + foreach ($data as $key => $value) { + if (!\is_string($key)) { + continue; + } + + $ret[$key[0]] = [ + 'value' => $value, + 'pattern' => $key, + ]; + } + + return $ret; + } + + /** + * Calculates the Unix timestamp based on the matched values by the reverse matching regular + * expression of parse(). + * + * @return bool|int The calculated timestamp or false if matched date is invalid + */ + private function calculateUnixTimestamp(\DateTime $dateTime, array $options) + { + $options = $this->getDefaultValueForOptions($options); + + $year = $options['year']; + $month = $options['month']; + $day = $options['day']; + $hour = $options['hour']; + $hourInstance = $options['hourInstance']; + $minute = $options['minute']; + $second = $options['second']; + $marker = $options['marker']; + $timezone = $options['timezone']; + + // If month is false, return immediately (intl behavior) + if (false === $month) { + Icu::setError(Icu::U_PARSE_ERROR, 'Date parsing failed'); + + return false; + } + + // Normalize hour + if ($hourInstance instanceof HourTransformer) { + $hour = $hourInstance->normalizeHour($hour, $marker); + } + + // Set the timezone if different from the default one + if (null !== $timezone && $timezone !== $this->timezone) { + $dateTime->setTimezone(new \DateTimeZone($timezone)); + } + + // Normalize yy year + preg_match_all($this->regExp, $this->pattern, $matches); + if (\in_array('yy', $matches[0])) { + $dateTime->setTimestamp(time()); + $year = $year > (int) $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year; + } + + $dateTime->setDate($year, $month, $day); + $dateTime->setTime($hour, $minute, $second); + + return $dateTime->getTimestamp(); + } + + /** + * Add sensible default values for missing items in the extracted date/time options array. The values + * are base in the beginning of the Unix era. + */ + private function getDefaultValueForOptions(array $options): array + { + return [ + 'year' => $options['year'] ?? 1970, + 'month' => $options['month'] ?? 1, + 'day' => $options['day'] ?? 1, + 'hour' => $options['hour'] ?? 0, + 'hourInstance' => $options['hourInstance'] ?? null, + 'minute' => $options['minute'] ?? 0, + 'second' => $options['second'] ?? 0, + 'marker' => $options['marker'] ?? null, + 'timezone' => $options['timezone'] ?? null, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/Hour1200Transformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/Hour1200Transformer.php new file mode 100644 index 0000000000..68891a79ae --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/Hour1200Transformer.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for 12 hour format (0-11). + * + * @author Igor Wiedler + * + * @internal + */ +class Hour1200Transformer extends HourTransformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $hourOfDay = $dateTime->format('g'); + $hourOfDay = '12' === $hourOfDay ? '0' : $hourOfDay; + + return $this->padLeft($hourOfDay, $length); + } + + public function normalizeHour(int $hour, ?string $marker = null): int + { + if ('PM' === $marker) { + $hour += 12; + } + + return $hour; + } + + public function getReverseMatchingRegExp(int $length): string + { + return '\d{1,2}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'hour' => (int) $matched, + 'hourInstance' => $this, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/Hour1201Transformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/Hour1201Transformer.php new file mode 100644 index 0000000000..4ac9b2a359 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/Hour1201Transformer.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for 12 hour format (1-12). + * + * @author Igor Wiedler + * + * @internal + */ +class Hour1201Transformer extends HourTransformer +{ + public function format(\DateTime $dateTime, int $length): string + { + return $this->padLeft($dateTime->format('g'), $length); + } + + public function normalizeHour(int $hour, ?string $marker = null): int + { + if ('PM' !== $marker && 12 === $hour) { + $hour = 0; + } elseif ('PM' === $marker && 12 !== $hour) { + // If PM and hour is not 12 (1-12), sum 12 hour + $hour += 12; + } + + return $hour; + } + + public function getReverseMatchingRegExp(int $length): string + { + return '\d{1,2}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'hour' => (int) $matched, + 'hourInstance' => $this, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/Hour2400Transformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/Hour2400Transformer.php new file mode 100644 index 0000000000..bc259e2884 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/Hour2400Transformer.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for 24 hour format (0-23). + * + * @author Igor Wiedler + * + * @internal + */ +class Hour2400Transformer extends HourTransformer +{ + public function format(\DateTime $dateTime, int $length): string + { + return $this->padLeft($dateTime->format('G'), $length); + } + + public function normalizeHour(int $hour, ?string $marker = null): int + { + if ('AM' === $marker) { + $hour = 0; + } elseif ('PM' === $marker) { + $hour = 12; + } + + return $hour; + } + + public function getReverseMatchingRegExp(int $length): string + { + return '\d{1,2}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'hour' => (int) $matched, + 'hourInstance' => $this, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/Hour2401Transformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/Hour2401Transformer.php new file mode 100644 index 0000000000..f8d3367b1c --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/Hour2401Transformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for 24 hour format (1-24). + * + * @author Igor Wiedler + * + * @internal + */ +class Hour2401Transformer extends HourTransformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $hourOfDay = $dateTime->format('G'); + $hourOfDay = '0' === $hourOfDay ? '24' : $hourOfDay; + + return $this->padLeft($hourOfDay, $length); + } + + public function normalizeHour(int $hour, ?string $marker = null): int + { + if ((null === $marker && 24 === $hour) || 'AM' === $marker) { + $hour = 0; + } elseif ('PM' === $marker) { + $hour = 12; + } + + return $hour; + } + + public function getReverseMatchingRegExp(int $length): string + { + return '\d{1,2}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'hour' => (int) $matched, + 'hourInstance' => $this, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/HourTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/HourTransformer.php new file mode 100644 index 0000000000..e973db1817 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/HourTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Base class for hour transformers. + * + * @author Eriksen Costa + * + * @internal + */ +abstract class HourTransformer extends Transformer +{ + /** + * Returns a normalized hour value suitable for the hour transformer type. + * + * @param int $hour The hour value + * @param string $marker An optional AM/PM marker + * + * @return int The normalized hour value + */ + abstract public function normalizeHour(int $hour, ?string $marker = null): int; +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/MinuteTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/MinuteTransformer.php new file mode 100644 index 0000000000..e8bddc6fd3 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/MinuteTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for minute format. + * + * @author Igor Wiedler + * + * @internal + */ +class MinuteTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $minuteOfHour = (int) $dateTime->format('i'); + + return $this->padLeft($minuteOfHour, $length); + } + + public function getReverseMatchingRegExp(int $length): string + { + return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'minute' => (int) $matched, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/MonthTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/MonthTransformer.php new file mode 100644 index 0000000000..6712ed2827 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/MonthTransformer.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for month format. + * + * @author Igor Wiedler + * + * @internal + */ +class MonthTransformer extends Transformer +{ + protected static $months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + /** + * Short months names (first 3 letters). + */ + protected static $shortMonths = []; + + /** + * Flipped $months array, $name => $index. + */ + protected static $flippedMonths = []; + + /** + * Flipped $shortMonths array, $name => $index. + */ + protected static $flippedShortMonths = []; + + public function __construct() + { + if (0 === \count(self::$shortMonths)) { + self::$shortMonths = array_map(function ($month) { + return substr($month, 0, 3); + }, self::$months); + + self::$flippedMonths = array_flip(self::$months); + self::$flippedShortMonths = array_flip(self::$shortMonths); + } + } + + public function format(\DateTime $dateTime, int $length): string + { + $matchLengthMap = [ + 1 => 'n', + 2 => 'm', + 3 => 'M', + 4 => 'F', + ]; + + if (isset($matchLengthMap[$length])) { + return $dateTime->format($matchLengthMap[$length]); + } + + if (5 === $length) { + return substr($dateTime->format('M'), 0, 1); + } + + return $this->padLeft($dateTime->format('m'), $length); + } + + public function getReverseMatchingRegExp(int $length): string + { + switch ($length) { + case 1: + $regExp = '\d{1,2}'; + break; + case 3: + $regExp = implode('|', self::$shortMonths); + break; + case 4: + $regExp = implode('|', self::$months); + break; + case 5: + $regExp = '[JFMASOND]'; + break; + default: + $regExp = '\d{1,'.$length.'}'; + break; + } + + return $regExp; + } + + public function extractDateOptions(string $matched, int $length): array + { + if (!is_numeric($matched)) { + if (3 === $length) { + $matched = self::$flippedShortMonths[$matched] + 1; + } elseif (4 === $length) { + $matched = self::$flippedMonths[$matched] + 1; + } elseif (5 === $length) { + // IntlDateFormatter::parse() always returns false for MMMMM or LLLLL + $matched = false; + } + } else { + $matched = (int) $matched; + } + + return [ + 'month' => $matched, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/QuarterTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/QuarterTransformer.php new file mode 100644 index 0000000000..a549deeda2 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/QuarterTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for quarter format. + * + * @author Igor Wiedler + * + * @internal + */ +class QuarterTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $month = (int) $dateTime->format('n'); + $quarter = (int) floor(($month - 1) / 3) + 1; + switch ($length) { + case 1: + case 2: + return $this->padLeft($quarter, $length); + case 3: + return 'Q'.$quarter; + case 4: + $map = [1 => '1st quarter', 2 => '2nd quarter', 3 => '3rd quarter', 4 => '4th quarter']; + + return $map[$quarter]; + default: + if (\defined('INTL_ICU_VERSION') && version_compare(\INTL_ICU_VERSION, '70.1', '<')) { + $map = [1 => '1st quarter', 2 => '2nd quarter', 3 => '3rd quarter', 4 => '4th quarter']; + + return $map[$quarter]; + } else { + return $quarter; + } + } + } + + public function getReverseMatchingRegExp(int $length): string + { + switch ($length) { + case 1: + case 2: + return '\d{'.$length.'}'; + case 3: + return 'Q\d'; + default: + return '(?:1st|2nd|3rd|4th) quarter'; + } + } + + public function extractDateOptions(string $matched, int $length): array + { + return []; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/SecondTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/SecondTransformer.php new file mode 100644 index 0000000000..fcb1028f4a --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/SecondTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for the second format. + * + * @author Igor Wiedler + * + * @internal + */ +class SecondTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + $secondOfMinute = (int) $dateTime->format('s'); + + return $this->padLeft($secondOfMinute, $length); + } + + public function getReverseMatchingRegExp(int $length): string + { + return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'second' => (int) $matched, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/TimezoneTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/TimezoneTransformer.php new file mode 100644 index 0000000000..bab7a96f8b --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/TimezoneTransformer.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\Polyfill\Intl\Icu\DateFormat; + +use Symfony\Polyfill\Intl\Icu\Exception\NotImplementedException; + +/** + * Parser and formatter for time zone format. + * + * @author Igor Wiedler + * + * @internal + */ +class TimezoneTransformer extends Transformer +{ + /** + * @throws NotImplementedException When time zone is different than UTC or GMT (Etc/GMT) + */ + public function format(\DateTime $dateTime, int $length): string + { + $timeZone = substr($dateTime->getTimezone()->getName(), 0, 3); + + if (!\in_array($timeZone, ['Etc', 'UTC', 'GMT'])) { + throw new NotImplementedException('Time zone different than GMT or UTC is not supported as a formatting output.'); + } + + if ('Etc' === $timeZone) { + // i.e. Etc/GMT+1, Etc/UTC, Etc/Zulu + $timeZone = substr($dateTime->getTimezone()->getName(), 4); + } + + // From ICU >= 59.1 GMT and UTC are no longer unified + if (\in_array($timeZone, ['UTC', 'UCT', 'Universal', 'Zulu'])) { + // offset is not supported with UTC + return $length > 3 ? 'Coordinated Universal Time' : 'UTC'; + } + + $offset = (int) $dateTime->format('O'); + + // From ICU >= 4.8, the zero offset is no more used, example: GMT instead of GMT+00:00 + if (0 === $offset) { + return $length > 3 ? 'Greenwich Mean Time' : 'GMT'; + } + + if ($length > 3) { + return $dateTime->format('\G\M\TP'); + } + + return sprintf('GMT%s%d', $offset >= 0 ? '+' : '', $offset / 100); + } + + public function getReverseMatchingRegExp(int $length): string + { + return 'GMT[+-]\d{2}:?\d{2}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'timezone' => self::getEtcTimeZoneId($matched), + ]; + } + + /** + * Get an Etc/GMT timezone identifier for the specified timezone. + * + * The PHP documentation for timezones states to not use the 'Other' time zones because them exists + * "for backwards compatibility". However all Etc/GMT time zones are in the tz database 'etcetera' file, + * which indicates they are not deprecated (neither are old names). + * + * Only GMT, Etc/Universal, Etc/Zulu, Etc/Greenwich, Etc/GMT-0, Etc/GMT+0 and Etc/GMT0 are old names and + * are linked to Etc/GMT or Etc/UTC. + * + * @param string $formattedTimeZone A GMT timezone string (GMT-03:00, e.g.) + * + * @return string A timezone identifier + * + * @see https://php.net/timezones.others + * + * @throws NotImplementedException When the GMT time zone have minutes offset different than zero + * @throws \InvalidArgumentException When the value can not be matched with pattern + */ + public static function getEtcTimeZoneId(string $formattedTimeZone): string + { + if (preg_match('/GMT(?P[+-])(?P\d{2}):?(?P\d{2})/', $formattedTimeZone, $matches)) { + $hours = (int) $matches['hours']; + $minutes = (int) $matches['minutes']; + $signal = '-' === $matches['signal'] ? '+' : '-'; + + if (0 < $minutes) { + throw new NotImplementedException(sprintf('It is not possible to use a GMT time zone with minutes offset different than zero (0). GMT time zone tried: "%s".', $formattedTimeZone)); + } + + return 'Etc/GMT'.(0 !== $hours ? $signal.$hours : ''); + } + + throw new \InvalidArgumentException(sprintf('The GMT time zone "%s" does not match with the supported formats GMT[+-]HH:MM or GMT[+-]HHMM.', $formattedTimeZone)); + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/Transformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/Transformer.php new file mode 100644 index 0000000000..7f8bf25b52 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/Transformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for date formats. + * + * @author Igor Wiedler + * + * @internal + */ +abstract class Transformer +{ + /** + * Format a value using a configured DateTime as date/time source. + * + * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value + * @param int $length The formatted value string length + * + * @return string The formatted value + */ + abstract public function format(\DateTime $dateTime, int $length): string; + + /** + * Returns a reverse matching regular expression of a string generated by format(). + * + * @param int $length The length of the value to be reverse matched + * + * @return string The reverse matching regular expression + */ + abstract public function getReverseMatchingRegExp(int $length): string; + + /** + * Extract date options from a matched value returned by the processing of the reverse matching + * regular expression. + * + * @param string $matched The matched value + * @param int $length The length of the Transformer pattern string + * + * @return array An associative array + */ + abstract public function extractDateOptions(string $matched, int $length): array; + + /** + * Pad a string with zeros to the left. + * + * @param string $value The string to be padded + * @param int $length The length to pad + * + * @return string The padded string + */ + protected function padLeft(string $value, int $length): string + { + return str_pad($value, $length, '0', \STR_PAD_LEFT); + } +} diff --git a/lib/symfony/polyfill-intl-icu/DateFormat/YearTransformer.php b/lib/symfony/polyfill-intl-icu/DateFormat/YearTransformer.php new file mode 100644 index 0000000000..a27ce8555f --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/DateFormat/YearTransformer.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\Polyfill\Intl\Icu\DateFormat; + +/** + * Parser and formatter for year format. + * + * @author Igor Wiedler + * + * @internal + */ +class YearTransformer extends Transformer +{ + public function format(\DateTime $dateTime, int $length): string + { + if (2 === $length) { + return $dateTime->format('y'); + } + + return $this->padLeft($dateTime->format('Y'), $length); + } + + public function getReverseMatchingRegExp(int $length): string + { + return 2 === $length ? '\d{2}' : '\d{1,4}'; + } + + public function extractDateOptions(string $matched, int $length): array + { + return [ + 'year' => (int) $matched, + ]; + } +} diff --git a/lib/symfony/polyfill-intl-icu/Exception/ExceptionInterface.php b/lib/symfony/polyfill-intl-icu/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..a453b5e2fc --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\Exception; + +/** + * Base ExceptionInterface for the Intl component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/lib/symfony/polyfill-intl-icu/Exception/MethodArgumentNotImplementedException.php b/lib/symfony/polyfill-intl-icu/Exception/MethodArgumentNotImplementedException.php new file mode 100644 index 0000000000..db120a340f --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Exception/MethodArgumentNotImplementedException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\Exception; + +/** + * @author Eriksen Costa + */ +class MethodArgumentNotImplementedException extends NotImplementedException +{ + /** + * @param string $methodName The method name that raised the exception + * @param string $argName The argument name that is not implemented + */ + public function __construct(string $methodName, string $argName) + { + $message = sprintf('The %s() method\'s argument $%s behavior is not implemented.', $methodName, $argName); + parent::__construct($message); + } +} diff --git a/lib/symfony/polyfill-intl-icu/Exception/MethodArgumentValueNotImplementedException.php b/lib/symfony/polyfill-intl-icu/Exception/MethodArgumentValueNotImplementedException.php new file mode 100644 index 0000000000..bd9204234e --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Exception/MethodArgumentValueNotImplementedException.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\Exception; + +/** + * @author Eriksen Costa + */ +class MethodArgumentValueNotImplementedException extends NotImplementedException +{ + /** + * @param string $methodName The method name that raised the exception + * @param string $argName The argument name + * @param mixed $argValue The argument value that is not implemented + * @param string $additionalMessage An optional additional message to append to the exception message + */ + public function __construct(string $methodName, string $argName, $argValue, string $additionalMessage = '') + { + $message = sprintf( + 'The %s() method\'s argument $%s value %s behavior is not implemented.%s', + $methodName, + $argName, + var_export($argValue, true), + '' !== $additionalMessage ? ' '.$additionalMessage.'. ' : '' + ); + + parent::__construct($message); + } +} diff --git a/lib/symfony/polyfill-intl-icu/Exception/MethodNotImplementedException.php b/lib/symfony/polyfill-intl-icu/Exception/MethodNotImplementedException.php new file mode 100644 index 0000000000..9e1a43985e --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Exception/MethodNotImplementedException.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\Polyfill\Intl\Icu\Exception; + +/** + * @author Eriksen Costa + */ +class MethodNotImplementedException extends NotImplementedException +{ + /** + * @param string $methodName The name of the method + */ + public function __construct(string $methodName) + { + parent::__construct(sprintf('The %s() is not implemented.', $methodName)); + } +} diff --git a/lib/symfony/polyfill-intl-icu/Exception/NotImplementedException.php b/lib/symfony/polyfill-intl-icu/Exception/NotImplementedException.php new file mode 100644 index 0000000000..929b9334d4 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Exception/NotImplementedException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\Exception; + +/** + * Base exception class for not implemented behaviors of the intl extension in the Locale component. + * + * @author Eriksen Costa + */ +class NotImplementedException extends RuntimeException +{ + public const INTL_INSTALL_MESSAGE = 'Please install the "intl" extension for full localization capabilities.'; + + /** + * @param string $message The exception message. A note to install the intl extension is appended to this string + */ + public function __construct(string $message) + { + parent::__construct($message.' '.self::INTL_INSTALL_MESSAGE); + } +} diff --git a/lib/symfony/polyfill-intl-icu/Exception/RuntimeException.php b/lib/symfony/polyfill-intl-icu/Exception/RuntimeException.php new file mode 100644 index 0000000000..ceedffe8ee --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu\Exception; + +/** + * RuntimeException for the Intl component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/lib/symfony/polyfill-intl-icu/Icu.php b/lib/symfony/polyfill-intl-icu/Icu.php new file mode 100644 index 0000000000..b9590f43d8 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Icu.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu; + +/** + * Provides fake static versions of the global functions in the intl extension. + * + * @author Bernhard Schussek + * + * @internal + */ +abstract class Icu +{ + /** + * Indicates that no error occurred. + */ + public const U_ZERO_ERROR = 0; + + /** + * Indicates that an invalid argument was passed. + */ + public const U_ILLEGAL_ARGUMENT_ERROR = 1; + + /** + * Indicates that the parse() operation failed. + */ + public const U_PARSE_ERROR = 9; + + /** + * All known error codes. + */ + private static $errorCodes = [ + self::U_ZERO_ERROR => 'U_ZERO_ERROR', + self::U_ILLEGAL_ARGUMENT_ERROR => 'U_ILLEGAL_ARGUMENT_ERROR', + self::U_PARSE_ERROR => 'U_PARSE_ERROR', + ]; + + /** + * The error code of the last operation. + */ + private static $errorCode = self::U_ZERO_ERROR; + + /** + * The error code of the last operation. + */ + private static $errorMessage = 'U_ZERO_ERROR'; + + /** + * Returns whether the error code indicates a failure. + * + * @param int $errorCode The error code returned by Icu::getErrorCode() + */ + public static function isFailure(int $errorCode): bool + { + return isset(self::$errorCodes[$errorCode]) + && $errorCode > self::U_ZERO_ERROR; + } + + /** + * Returns the error code of the last operation. + * + * Returns Icu::U_ZERO_ERROR if no error occurred. + * + * @return int + */ + public static function getErrorCode() + { + return self::$errorCode; + } + + /** + * Returns the error message of the last operation. + * + * Returns "U_ZERO_ERROR" if no error occurred. + */ + public static function getErrorMessage(): string + { + return self::$errorMessage; + } + + /** + * Returns the symbolic name for a given error code. + * + * @param int $code The error code returned by Icu::getErrorCode() + */ + public static function getErrorName(int $code): string + { + return self::$errorCodes[$code] ?? '[BOGUS UErrorCode]'; + } + + /** + * Sets the current error. + * + * @param int $code One of the error constants in this class + * @param string $message The ICU class error message + * + * @throws \InvalidArgumentException If the code is not one of the error constants in this class + */ + public static function setError(int $code, string $message = '') + { + if (!isset(self::$errorCodes[$code])) { + throw new \InvalidArgumentException(sprintf('No such error code: "%s".', $code)); + } + + self::$errorMessage = $message ? sprintf('%s: %s', $message, self::$errorCodes[$code]) : self::$errorCodes[$code]; + self::$errorCode = $code; + } +} diff --git a/lib/symfony/polyfill-intl-icu/IntlDateFormatter.php b/lib/symfony/polyfill-intl-icu/IntlDateFormatter.php new file mode 100644 index 0000000000..b2674f906e --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/IntlDateFormatter.php @@ -0,0 +1,645 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu; + +use Symfony\Polyfill\Intl\Icu\DateFormat\FullTransformer; +use Symfony\Polyfill\Intl\Icu\Exception\MethodArgumentNotImplementedException; +use Symfony\Polyfill\Intl\Icu\Exception\MethodArgumentValueNotImplementedException; +use Symfony\Polyfill\Intl\Icu\Exception\MethodNotImplementedException; + +/** + * Replacement for PHP's native {@link \IntlDateFormatter} class. + * + * The only methods currently supported in this class are: + * + * - {@link __construct} + * - {@link create} + * - {@link format} + * - {@link getCalendar} + * - {@link getDateType} + * - {@link getErrorCode} + * - {@link getErrorMessage} + * - {@link getLocale} + * - {@link getPattern} + * - {@link getTimeType} + * - {@link getTimeZoneId} + * - {@link isLenient} + * - {@link parse} + * - {@link setLenient} + * - {@link setPattern} + * - {@link setTimeZoneId} + * - {@link setTimeZone} + * + * @author Igor Wiedler + * @author Bernhard Schussek + * + * @internal + */ +abstract class IntlDateFormatter +{ + /** + * The error code from the last operation. + * + * @var int + */ + protected $errorCode = Icu::U_ZERO_ERROR; + + /** + * The error message from the last operation. + * + * @var string + */ + protected $errorMessage = 'U_ZERO_ERROR'; + + /* date/time format types */ + public const NONE = -1; + public const FULL = 0; + public const LONG = 1; + public const MEDIUM = 2; + public const SHORT = 3; + + /* date format types */ + public const RELATIVE_FULL = 128; + public const RELATIVE_LONG = 129; + public const RELATIVE_MEDIUM = 130; + public const RELATIVE_SHORT = 131; + + /* calendar formats */ + public const TRADITIONAL = 0; + public const GREGORIAN = 1; + + /** + * Patterns used to format the date when no pattern is provided. + */ + private $defaultDateFormats = [ + self::NONE => '', + self::FULL => 'EEEE, MMMM d, y', + self::LONG => 'MMMM d, y', + self::MEDIUM => 'MMM d, y', + self::SHORT => 'M/d/yy', + self::RELATIVE_FULL => 'EEEE, MMMM d, y', + self::RELATIVE_LONG => 'MMMM d, y', + self::RELATIVE_MEDIUM => 'MMM d, y', + self::RELATIVE_SHORT => 'M/d/yy', + ]; + + /** + * Patterns used to format the time when no pattern is provided. + */ + private $defaultTimeFormats = [ + self::FULL => 'h:mm:ss a zzzz', + self::LONG => 'h:mm:ss a z', + self::MEDIUM => 'h:mm:ss a', + self::SHORT => 'h:mm a', + ]; + + private $dateType; + private $timeType; + + /** + * @var string + */ + private $pattern; + + /** + * @var \DateTimeZone + */ + private $dateTimeZone; + + /** + * @var bool + */ + private $uninitializedTimeZoneId = false; + + /** + * @var string + */ + private $timezoneId; + + /** + * @var bool + */ + private $isRelativeDateType = false; + + /** + * @param string|null $locale The locale code. The only currently supported locale is "en" (or null using the default locale, i.e. "en") + * @param \IntlTimeZone|\DateTimeZone|string|null $timezone Timezone identifier + * @param \IntlCalendar|int|null $calendar Calendar to use for formatting or parsing. The only currently + * supported value is IntlDateFormatter::GREGORIAN (or null using the default calendar, i.e. "GREGORIAN") + * + * @see https://php.net/intldateformatter.create + * @see http://userguide.icu-project.org/formatparse/datetime + * + * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed + * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed + */ + public function __construct(?string $locale, ?int $dateType, ?int $timeType, $timezone = null, $calendar = null, ?string $pattern = '') + { + if ('en' !== $locale && null !== $locale) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported'); + } + + if (self::GREGORIAN !== $calendar && null !== $calendar) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'calendar', $calendar, 'Only the GREGORIAN calendar is supported'); + } + + if (\PHP_VERSION_ID >= 80100) { + if (null === $dateType) { + @trigger_error('Passing null to parameter #2 ($dateType) of type int is deprecated', \E_USER_DEPRECATED); + } + + if (null === $timeType) { + @trigger_error('Passing null to parameter #3 ($timeType) of type int is deprecated', \E_USER_DEPRECATED); + } + } + + $this->dateType = $dateType ?? self::FULL; + $this->timeType = $timeType ?? self::FULL; + + if ('' === ($pattern ?? '')) { + $pattern = $this->getDefaultPattern(); + } + + $this->setPattern($pattern); + $this->setTimeZone($timezone); + + if (\in_array($this->dateType, [self::RELATIVE_FULL, self::RELATIVE_LONG, self::RELATIVE_MEDIUM, self::RELATIVE_SHORT], true)) { + $this->isRelativeDateType = true; + } + } + + /** + * Static constructor. + * + * @param string|null $locale The locale code. The only currently supported locale is "en" (or null using the default locale, i.e. "en") + * @param \IntlTimeZone|\DateTimeZone|string|null $timezone Timezone identifier + * @param \IntlCalendar|int|null $calendar Calendar to use for formatting or parsing; default is Gregorian + * One of the calendar constants + * + * @return static + * + * @see https://php.net/intldateformatter.create + * @see http://userguide.icu-project.org/formatparse/datetime + * + * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed + * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed + */ + public static function create(?string $locale, ?int $dateType, ?int $timeType, $timezone = null, ?int $calendar = null, ?string $pattern = '') + { + return new static($locale, $dateType, $timeType, $timezone, $calendar, $pattern); + } + + /** + * Format the date/time value (timestamp) as a string. + * + * @param int|string|\DateTimeInterface $datetime The timestamp to format + * + * @return string|false The formatted value or false if formatting failed + * + * @see https://php.net/intldateformatter.format + * + * @throws MethodArgumentValueNotImplementedException If one of the formatting characters is not implemented + */ + public function format($datetime) + { + // intl allows timestamps to be passed as arrays - we don't + if (\is_array($datetime)) { + $message = 'Only Unix timestamps and DateTime objects are supported'; + + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'datetime', $datetime, $message); + } + + if (\is_string($datetime) && $dt = \DateTime::createFromFormat('U', $datetime)) { + $datetime = $dt; + } + + // behave like the intl extension + $argumentError = null; + if (!\is_int($datetime) && !$datetime instanceof \DateTimeInterface) { + $argumentError = sprintf('datefmt_format: string \'%s\' is not numeric, which would be required for it to be a valid date', $datetime); + } + + if (null !== $argumentError) { + Icu::setError(Icu::U_ILLEGAL_ARGUMENT_ERROR, $argumentError); + $this->errorCode = Icu::getErrorCode(); + $this->errorMessage = Icu::getErrorMessage(); + + return false; + } + + if ($datetime instanceof \DateTimeInterface) { + $datetime = $datetime->format('U'); + } + + $pattern = $this->getPattern(); + $formatted = ''; + + if ($this->isRelativeDateType && $formatted = $this->getRelativeDateFormat($datetime)) { + if (self::NONE === $this->timeType) { + $pattern = ''; + } else { + $pattern = $this->defaultTimeFormats[$this->timeType]; + if (\in_array($this->dateType, [self::RELATIVE_MEDIUM, self::RELATIVE_SHORT], true)) { + $formatted .= ', '; + } else { + $formatted .= ' at '; + } + } + } + + $transformer = new FullTransformer($pattern, $this->getTimeZoneId()); + $formatted .= $transformer->format($this->createDateTime($datetime)); + + // behave like the intl extension + Icu::setError(Icu::U_ZERO_ERROR); + $this->errorCode = Icu::getErrorCode(); + $this->errorMessage = Icu::getErrorMessage(); + + return $formatted; + } + + /** + * Not supported. Formats an object. + * + * @return string The formatted value + * + * @see https://php.net/intldateformatter.formatobject + * + * @throws MethodNotImplementedException + */ + public static function formatObject($datetime, $format = null, ?string $locale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the formatter's calendar. + * + * @return int The calendar being used by the formatter. Currently always returns + * IntlDateFormatter::GREGORIAN. + * + * @see https://php.net/intldateformatter.getcalendar + */ + public function getCalendar() + { + return self::GREGORIAN; + } + + /** + * Not supported. Returns the formatter's calendar object. + * + * @return object The calendar's object being used by the formatter + * + * @see https://php.net/intldateformatter.getcalendarobject + * + * @throws MethodNotImplementedException + */ + public function getCalendarObject() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the formatter's datetype. + * + * @return int The current value of the formatter + * + * @see https://php.net/intldateformatter.getdatetype + */ + public function getDateType() + { + return $this->dateType; + } + + /** + * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value. + * + * @return int The error code from last formatter call + * + * @see https://php.net/intldateformatter.geterrorcode + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value. + * + * @return string The error message from last formatter call + * + * @see https://php.net/intldateformatter.geterrormessage + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Returns the formatter's locale. + * + * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE) + * + * @return string The locale used to create the formatter. Currently always + * returns "en". + * + * @see https://php.net/intldateformatter.getlocale + */ + public function getLocale(int $type = Locale::ACTUAL_LOCALE) + { + return 'en'; + } + + /** + * Returns the formatter's pattern. + * + * @return string The pattern string used by the formatter + * + * @see https://php.net/intldateformatter.getpattern + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Returns the formatter's time type. + * + * @return int The time type used by the formatter + * + * @see https://php.net/intldateformatter.gettimetype + */ + public function getTimeType() + { + return $this->timeType; + } + + /** + * Returns the formatter's timezone identifier. + * + * @return string The timezone identifier used by the formatter + * + * @see https://php.net/intldateformatter.gettimezoneid + */ + public function getTimeZoneId() + { + if (!$this->uninitializedTimeZoneId) { + return $this->timezoneId; + } + + return date_default_timezone_get(); + } + + /** + * Not supported. Returns the formatter's timezone. + * + * @return mixed The timezone used by the formatter + * + * @see https://php.net/intldateformatter.gettimezone + * + * @throws MethodNotImplementedException + */ + public function getTimeZone() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns whether the formatter is lenient. + * + * @return bool Currently always returns false + * + * @see https://php.net/intldateformatter.islenient + * + * @throws MethodNotImplementedException + */ + public function isLenient() + { + return false; + } + + /** + * Not supported. Parse string to a field-based time value. + * + * @return string Localtime compatible array of integers: contains 24 hour clock value in tm_hour field + * + * @see https://php.net/intldateformatter.localtime + * + * @throws MethodNotImplementedException + */ + public function localtime(string $string, &$offset = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Parse string to a timestamp value. + * + * @return int|false Parsed value as a timestamp + * + * @see https://php.net/intldateformatter.parse + * + * @throws MethodArgumentNotImplementedException When $offset different than null, behavior not implemented + */ + public function parse(string $string, &$offset = null) + { + // We don't calculate the position when parsing the value + if (null !== $offset) { + throw new MethodArgumentNotImplementedException(__METHOD__, 'offset'); + } + + $dateTime = $this->createDateTime(0); + $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId()); + + $timestamp = $transformer->parse($dateTime, $string); + + // behave like the intl extension. FullTransformer::parse() set the proper error + $this->errorCode = Icu::getErrorCode(); + $this->errorMessage = Icu::getErrorMessage(); + + return $timestamp; + } + + /** + * Not supported. Set the formatter's calendar. + * + * @param \IntlCalendar|int|null $calendar + * + * @return bool true on success or false on failure + * + * @see https://php.net/intldateformatter.setcalendar + * + * @throws MethodNotImplementedException + */ + public function setCalendar($calendar) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set the leniency of the parser. + * + * Define if the parser is strict or lenient in interpreting inputs that do not match the pattern + * exactly. Enabling lenient parsing allows the parser to accept otherwise flawed date or time + * patterns, parsing as much as possible to obtain a value. Extra space, unrecognized tokens, or + * invalid values ("February 30th") are not accepted. + * + * @param bool $lenient Sets whether the parser is lenient or not. Currently + * only false (strict) is supported. + * + * @return bool true on success or false on failure + * + * @see https://php.net/intldateformatter.setlenient + * + * @throws MethodArgumentValueNotImplementedException When $lenient is true + */ + public function setLenient(bool $lenient) + { + if ($lenient) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'lenient', $lenient, 'Only the strict parser is supported'); + } + + return true; + } + + /** + * Set the formatter's pattern. + * + * @return bool true on success or false on failure + * + * @see https://php.net/intldateformatter.setpattern + * @see http://userguide.icu-project.org/formatparse/datetime + */ + public function setPattern(string $pattern) + { + $this->pattern = $pattern; + + return true; + } + + /** + * Sets formatterʼs timezone. + * + * @param \IntlTimeZone|\DateTimeZone|string|null $timezone + * + * @return bool true on success or false on failure + * + * @see https://php.net/intldateformatter.settimezone + */ + public function setTimeZone($timezone) + { + if ($timezone instanceof \IntlTimeZone) { + $timezone = $timezone->getID(); + } + + if ($timezone instanceof \DateTimeZone) { + $timezone = $timezone->getName(); + + // DateTimeZone returns the GMT offset timezones without the leading GMT, while our parsing requires it. + if (!empty($timezone) && ('+' === $timezone[0] || '-' === $timezone[0])) { + $timezone = 'GMT'.$timezone; + } + } + + if (null === $timezone) { + $timezone = date_default_timezone_get(); + + $this->uninitializedTimeZoneId = true; + } + + // Backup original passed time zone + $timezoneId = $timezone; + + // Get an Etc/GMT time zone that is accepted for \DateTimeZone + if ('GMT' !== $timezone && 0 === strpos($timezone, 'GMT')) { + try { + $timezone = DateFormat\TimezoneTransformer::getEtcTimeZoneId($timezone); + } catch (\InvalidArgumentException $e) { + // Does nothing, will fallback to UTC + } + } + + try { + $this->dateTimeZone = new \DateTimeZone($timezone); + if ('GMT' !== $timezone && $this->dateTimeZone->getName() !== $timezone) { + $timezoneId = $this->getTimeZoneId(); + } + } catch (\Exception $e) { + $timezoneId = $timezone = $this->getTimeZoneId(); + $this->dateTimeZone = new \DateTimeZone($timezone); + } + + $this->timezoneId = $timezoneId; + + return true; + } + + /** + * Create and returns a DateTime object with the specified timestamp and with the + * current time zone. + * + * @return \DateTime + */ + protected function createDateTime($timestamp) + { + $dateTime = \DateTime::createFromFormat('U', $timestamp); + $dateTime->setTimezone($this->dateTimeZone); + + return $dateTime; + } + + /** + * Returns a pattern string based in the datetype and timetype values. + * + * @return string + */ + protected function getDefaultPattern() + { + $pattern = ''; + if (self::NONE !== $this->dateType) { + $pattern = $this->defaultDateFormats[$this->dateType]; + } + if (self::NONE !== $this->timeType) { + if (\in_array($this->dateType, [self::FULL, self::LONG, self::RELATIVE_FULL, self::RELATIVE_LONG], true)) { + $pattern .= ' \'at\' '; + } elseif (self::NONE !== $this->dateType) { + $pattern .= ', '; + } + $pattern .= $this->defaultTimeFormats[$this->timeType]; + } + + return $pattern; + } + + private function getRelativeDateFormat(int $timestamp): string + { + $today = $this->createDateTime(time()); + $today->setTime(0, 0, 0); + + $datetime = $this->createDateTime($timestamp); + $datetime->setTime(0, 0, 0); + + $interval = $today->diff($datetime); + + if (false !== $interval) { + if (0 === $interval->days) { + return 'today'; + } + + if (1 === $interval->days) { + return 1 === $interval->invert ? 'yesterday' : 'tomorrow'; + } + } + + return ''; + } +} diff --git a/lib/symfony/polyfill-intl-icu/LICENSE b/lib/symfony/polyfill-intl-icu/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/polyfill-intl-icu/Locale.php b/lib/symfony/polyfill-intl-icu/Locale.php new file mode 100644 index 0000000000..f449fd5dff --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Locale.php @@ -0,0 +1,310 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu; + +use Symfony\Polyfill\Intl\Icu\Exception\MethodNotImplementedException; + +/** + * Replacement for PHP's native {@link \Locale} class. + * + * The only methods supported in this class are `getDefault` and `canonicalize`. + * All other methods will throw an exception when used. + * + * @author Eriksen Costa + * @author Bernhard Schussek + * + * @internal + */ +abstract class Locale +{ + public const DEFAULT_LOCALE = null; + + /* Locale method constants */ + public const ACTUAL_LOCALE = 0; + public const VALID_LOCALE = 1; + + /* Language tags constants */ + public const LANG_TAG = 'language'; + public const EXTLANG_TAG = 'extlang'; + public const SCRIPT_TAG = 'script'; + public const REGION_TAG = 'region'; + public const VARIANT_TAG = 'variant'; + public const GRANDFATHERED_LANG_TAG = 'grandfathered'; + public const PRIVATE_TAG = 'private'; + + /** + * Not supported. Returns the best available locale based on HTTP "Accept-Language" header according to RFC 2616. + * + * @return string The corresponding locale code + * + * @see https://php.net/locale.acceptfromhttp + * + * @throws MethodNotImplementedException + */ + public static function acceptFromHttp(string $header) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns a canonicalized locale string. + * + * This polyfill doesn't implement the full-spec algorithm. It only + * canonicalizes locale strings handled by the `LocaleBundle` class. + * + * @return string + */ + public static function canonicalize(string $locale) + { + if ('' === $locale || '.' === $locale[0]) { + return self::getDefault(); + } + + if (!preg_match('/^([a-z]{2})[-_]([a-z]{2})(?:([a-z]{2})(?:[-_]([a-z]{2}))?)?(?:\..*)?$/i', $locale, $m)) { + return $locale; + } + + if (!empty($m[4])) { + return strtolower($m[1]).'_'.ucfirst(strtolower($m[2].$m[3])).'_'.strtoupper($m[4]); + } + + if (!empty($m[3])) { + return strtolower($m[1]).'_'.ucfirst(strtolower($m[2].$m[3])); + } + + return strtolower($m[1]).'_'.strtoupper($m[2]); + } + + /** + * Not supported. Returns a correctly ordered and delimited locale code. + * + * @return string The corresponding locale code + * + * @see https://php.net/locale.composelocale + * + * @throws MethodNotImplementedException + */ + public static function composeLocale(array $subtags) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Checks if a language tag filter matches with locale. + * + * @return string The corresponding locale code + * + * @see https://php.net/locale.filtermatches + * + * @throws MethodNotImplementedException + */ + public static function filterMatches(string $languageTag, string $locale, bool $canonicalize = false) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the variants for the input locale. + * + * @return array The locale variants + * + * @see https://php.net/locale.getallvariants + * + * @throws MethodNotImplementedException + */ + public static function getAllVariants(string $locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Returns the default locale. + * + * @return string The default locale code. Always returns 'en' + * + * @see https://php.net/locale.getdefault + */ + public static function getDefault() + { + return 'en'; + } + + /** + * Not supported. Returns the localized display name for the locale language. + * + * @return string The localized language display name + * + * @see https://php.net/locale.getdisplaylanguage + * + * @throws MethodNotImplementedException + */ + public static function getDisplayLanguage(string $locale, ?string $displayLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the localized display name for the locale. + * + * @return string The localized locale display name + * + * @see https://php.net/locale.getdisplayname + * + * @throws MethodNotImplementedException + */ + public static function getDisplayName(string $locale, ?string $displayLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the localized display name for the locale region. + * + * @return string The localized region display name + * + * @see https://php.net/locale.getdisplayregion + * + * @throws MethodNotImplementedException + */ + public static function getDisplayRegion(string $locale, ?string $displayLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the localized display name for the locale script. + * + * @return string The localized script display name + * + * @see https://php.net/locale.getdisplayscript + * + * @throws MethodNotImplementedException + */ + public static function getDisplayScript(string $locale, ?string $displayLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the localized display name for the locale variant. + * + * @return string The localized variant display name + * + * @see https://php.net/locale.getdisplayvariant + * + * @throws MethodNotImplementedException + */ + public static function getDisplayVariant(string $locale, ?string $displayLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the keywords for the locale. + * + * @return array Associative array with the extracted variants + * + * @see https://php.net/locale.getkeywords + * + * @throws MethodNotImplementedException + */ + public static function getKeywords(string $locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the primary language for the locale. + * + * @return string|null The extracted language code or null in case of error + * + * @see https://php.net/locale.getprimarylanguage + * + * @throws MethodNotImplementedException + */ + public static function getPrimaryLanguage(string $locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the region for the locale. + * + * @return string|null The extracted region code or null if not present + * + * @see https://php.net/locale.getregion + * + * @throws MethodNotImplementedException + */ + public static function getRegion(string $locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the script for the locale. + * + * @return string|null The extracted script code or null if not present + * + * @see https://php.net/locale.getscript + * + * @throws MethodNotImplementedException + */ + public static function getScript(string $locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns the closest language tag for the locale. + * + * @see https://php.net/locale.lookup + * + * @throws MethodNotImplementedException + */ + public static function lookup(array $languageTag, string $locale, bool $canonicalize = false, ?string $defaultLocale = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns an associative array of locale identifier subtags. + * + * @return array|null Associative array with the extracted subtags + * + * @see https://php.net/locale.parselocale + * + * @throws MethodNotImplementedException + */ + public static function parseLocale(string $locale) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Sets the default runtime locale. + * + * @return bool true on success or false on failure + * + * @see https://php.net/locale.setdefault + * + * @throws MethodNotImplementedException + */ + public static function setDefault(string $locale) + { + if ('en' !== $locale) { + throw new MethodNotImplementedException(__METHOD__); + } + + return true; + } +} diff --git a/lib/symfony/polyfill-intl-icu/NumberFormatter.php b/lib/symfony/polyfill-intl-icu/NumberFormatter.php new file mode 100644 index 0000000000..cec375db73 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/NumberFormatter.php @@ -0,0 +1,835 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Icu; + +use Symfony\Polyfill\Intl\Icu\Exception\MethodArgumentNotImplementedException; +use Symfony\Polyfill\Intl\Icu\Exception\MethodArgumentValueNotImplementedException; +use Symfony\Polyfill\Intl\Icu\Exception\MethodNotImplementedException; +use Symfony\Polyfill\Intl\Icu\Exception\NotImplementedException; + +/** + * Replacement for PHP's native {@link \NumberFormatter} class. + * + * The only methods currently supported in this class are: + * + * - {@link __construct} + * - {@link create} + * - {@link formatCurrency} + * - {@link format} + * - {@link getAttribute} + * - {@link getErrorCode} + * - {@link getErrorMessage} + * - {@link getLocale} + * - {@link parse} + * - {@link setAttribute} + * + * @author Eriksen Costa + * @author Bernhard Schussek + * + * @internal + */ +abstract class NumberFormatter +{ + /* Format style constants */ + public const PATTERN_DECIMAL = 0; + public const DECIMAL = 1; + public const CURRENCY = 2; + public const PERCENT = 3; + public const SCIENTIFIC = 4; + public const SPELLOUT = 5; + public const ORDINAL = 6; + public const DURATION = 7; + public const PATTERN_RULEBASED = 9; + public const IGNORE = 0; + public const DEFAULT_STYLE = 1; + + /* Format type constants */ + public const TYPE_DEFAULT = 0; + public const TYPE_INT32 = 1; + public const TYPE_INT64 = 2; + public const TYPE_DOUBLE = 3; + public const TYPE_CURRENCY = 4; + + /* Numeric attribute constants */ + public const PARSE_INT_ONLY = 0; + public const GROUPING_USED = 1; + public const DECIMAL_ALWAYS_SHOWN = 2; + public const MAX_INTEGER_DIGITS = 3; + public const MIN_INTEGER_DIGITS = 4; + public const INTEGER_DIGITS = 5; + public const MAX_FRACTION_DIGITS = 6; + public const MIN_FRACTION_DIGITS = 7; + public const FRACTION_DIGITS = 8; + public const MULTIPLIER = 9; + public const GROUPING_SIZE = 10; + public const ROUNDING_MODE = 11; + public const ROUNDING_INCREMENT = 12; + public const FORMAT_WIDTH = 13; + public const PADDING_POSITION = 14; + public const SECONDARY_GROUPING_SIZE = 15; + public const SIGNIFICANT_DIGITS_USED = 16; + public const MIN_SIGNIFICANT_DIGITS = 17; + public const MAX_SIGNIFICANT_DIGITS = 18; + public const LENIENT_PARSE = 19; + + /* Text attribute constants */ + public const POSITIVE_PREFIX = 0; + public const POSITIVE_SUFFIX = 1; + public const NEGATIVE_PREFIX = 2; + public const NEGATIVE_SUFFIX = 3; + public const PADDING_CHARACTER = 4; + public const CURRENCY_CODE = 5; + public const DEFAULT_RULESET = 6; + public const PUBLIC_RULESETS = 7; + + /* Format symbol constants */ + public const DECIMAL_SEPARATOR_SYMBOL = 0; + public const GROUPING_SEPARATOR_SYMBOL = 1; + public const PATTERN_SEPARATOR_SYMBOL = 2; + public const PERCENT_SYMBOL = 3; + public const ZERO_DIGIT_SYMBOL = 4; + public const DIGIT_SYMBOL = 5; + public const MINUS_SIGN_SYMBOL = 6; + public const PLUS_SIGN_SYMBOL = 7; + public const CURRENCY_SYMBOL = 8; + public const INTL_CURRENCY_SYMBOL = 9; + public const MONETARY_SEPARATOR_SYMBOL = 10; + public const EXPONENTIAL_SYMBOL = 11; + public const PERMILL_SYMBOL = 12; + public const PAD_ESCAPE_SYMBOL = 13; + public const INFINITY_SYMBOL = 14; + public const NAN_SYMBOL = 15; + public const SIGNIFICANT_DIGIT_SYMBOL = 16; + public const MONETARY_GROUPING_SEPARATOR_SYMBOL = 17; + + /* Rounding mode values used by NumberFormatter::setAttribute() with NumberFormatter::ROUNDING_MODE attribute */ + public const ROUND_CEILING = 0; + public const ROUND_FLOOR = 1; + public const ROUND_DOWN = 2; + public const ROUND_UP = 3; + public const ROUND_HALFEVEN = 4; + public const ROUND_HALFDOWN = 5; + public const ROUND_HALFUP = 6; + + /* Pad position values used by NumberFormatter::setAttribute() with NumberFormatter::PADDING_POSITION attribute */ + public const PAD_BEFORE_PREFIX = 0; + public const PAD_AFTER_PREFIX = 1; + public const PAD_BEFORE_SUFFIX = 2; + public const PAD_AFTER_SUFFIX = 3; + + /** + * The error code from the last operation. + * + * @var int + */ + protected $errorCode = Icu::U_ZERO_ERROR; + + /** + * The error message from the last operation. + * + * @var string + */ + protected $errorMessage = 'U_ZERO_ERROR'; + + /** + * @var int + */ + private $style; + + /** + * Default values for the en locale. + */ + private $attributes = [ + self::FRACTION_DIGITS => 0, + self::GROUPING_USED => 1, + self::ROUNDING_MODE => self::ROUND_HALFEVEN, + ]; + + /** + * Holds the initialized attributes code. + */ + private $initializedAttributes = []; + + /** + * The supported styles to the constructor $styles argument. + */ + private static $supportedStyles = [ + 'CURRENCY' => self::CURRENCY, + 'DECIMAL' => self::DECIMAL, + ]; + + /** + * Supported attributes to the setAttribute() $attr argument. + */ + private static $supportedAttributes = [ + 'FRACTION_DIGITS' => self::FRACTION_DIGITS, + 'GROUPING_USED' => self::GROUPING_USED, + 'ROUNDING_MODE' => self::ROUNDING_MODE, + ]; + + /** + * The available rounding modes for setAttribute() usage with + * NumberFormatter::ROUNDING_MODE. NumberFormatter::ROUND_DOWN + * and NumberFormatter::ROUND_UP does not have a PHP only equivalent. + */ + private static $roundingModes = [ + 'ROUND_HALFEVEN' => self::ROUND_HALFEVEN, + 'ROUND_HALFDOWN' => self::ROUND_HALFDOWN, + 'ROUND_HALFUP' => self::ROUND_HALFUP, + 'ROUND_CEILING' => self::ROUND_CEILING, + 'ROUND_FLOOR' => self::ROUND_FLOOR, + 'ROUND_DOWN' => self::ROUND_DOWN, + 'ROUND_UP' => self::ROUND_UP, + ]; + + /** + * The mapping between NumberFormatter rounding modes to the available + * modes in PHP's round() function. + * + * @see https://php.net/round + */ + private static $phpRoundingMap = [ + self::ROUND_HALFDOWN => \PHP_ROUND_HALF_DOWN, + self::ROUND_HALFEVEN => \PHP_ROUND_HALF_EVEN, + self::ROUND_HALFUP => \PHP_ROUND_HALF_UP, + ]; + + /** + * The list of supported rounding modes which aren't available modes in + * PHP's round() function, but there's an equivalent. Keys are rounding + * modes, values does not matter. + */ + private static $customRoundingList = [ + self::ROUND_CEILING => true, + self::ROUND_FLOOR => true, + self::ROUND_DOWN => true, + self::ROUND_UP => true, + ]; + + /** + * The maximum value of the integer type in 32 bit platforms. + */ + private static $int32Max = 2147483647; + + /** + * The maximum value of the integer type in 64 bit platforms. + * + * @var int|float + */ + private static $int64Max = 9223372036854775807; + + private static $enSymbols = [ + self::DECIMAL => ['.', ',', ';', '%', '0', '#', '-', '+', '¤', '¤¤', '.', 'E', '‰', '*', '∞', 'NaN', '@', ','], + self::CURRENCY => ['.', ',', ';', '%', '0', '#', '-', '+', '¤', '¤¤', '.', 'E', '‰', '*', '∞', 'NaN', '@', ','], + ]; + + private static $enTextAttributes = [ + self::DECIMAL => ['', '', '-', '', ' ', 'XXX', ''], + self::CURRENCY => ['¤', '', '-¤', '', ' ', 'XXX'], + ]; + + /** + * @param string|null $locale The locale code. The only currently supported locale is "en" (or null using the default locale, i.e. "en") + * @param int $style Style of the formatting, one of the format style constants. + * The only supported styles are NumberFormatter::DECIMAL + * and NumberFormatter::CURRENCY. + * @param string $pattern Not supported. A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or + * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax + * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation + * + * @see https://php.net/numberformatter.create + * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classicu_1_1DecimalFormat.html#details + * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classicu_1_1RuleBasedNumberFormat.html#details + * + * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed + * @throws MethodArgumentValueNotImplementedException When the $style is not supported + * @throws MethodArgumentNotImplementedException When the pattern value is different than null + */ + public function __construct(?string $locale = 'en', ?int $style = null, ?string $pattern = null) + { + if ('en' !== $locale && null !== $locale) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported'); + } + + if (!\in_array($style, self::$supportedStyles)) { + $message = sprintf('The available styles are: %s.', implode(', ', array_keys(self::$supportedStyles))); + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'style', $style, $message); + } + + if (null !== $pattern) { + throw new MethodArgumentNotImplementedException(__METHOD__, 'pattern'); + } + + $this->style = $style; + } + + /** + * Static constructor. + * + * @param string|null $locale The locale code. The only supported locale is "en" (or null using the default locale, i.e. "en") + * @param int $style Style of the formatting, one of the format style constants. + * The only currently supported styles are NumberFormatter::DECIMAL + * and NumberFormatter::CURRENCY. + * @param string $pattern Not supported. A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or + * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax + * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation + * + * @return static + * + * @see https://php.net/numberformatter.create + * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details + * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details + * + * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed + * @throws MethodArgumentValueNotImplementedException When the $style is not supported + * @throws MethodArgumentNotImplementedException When the pattern value is different than null + */ + public static function create(?string $locale = 'en', ?int $style = null, ?string $pattern = null) + { + return new static($locale, $style, $pattern); + } + + /** + * Format a currency value. + * + * @return string The formatted currency value + * + * @see https://php.net/numberformatter.formatcurrency + * @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes + */ + public function formatCurrency(float $amount, string $currency) + { + if (self::DECIMAL === $this->style) { + return $this->format($amount); + } + + if (null === $symbol = Currencies::getSymbol($currency)) { + return false; + } + $fractionDigits = Currencies::getFractionDigits($currency); + + $amount = $this->roundCurrency($amount, $currency); + + $negative = false; + if (0 > $amount) { + $negative = true; + $amount *= -1; + } + + $amount = $this->formatNumber($amount, $fractionDigits); + + // There's a non-breaking space after the currency code (i.e. CRC 100), but not if the currency has a symbol (i.e. £100). + $ret = $symbol.(mb_strlen($symbol, 'UTF-8') > 2 ? "\xc2\xa0" : '').$amount; + + return $negative ? '-'.$ret : $ret; + } + + /** + * Format a number. + * + * @param int|float $num The value to format + * @param int $type Type of the formatting, one of the format type constants. + * Only type NumberFormatter::TYPE_DEFAULT is currently supported. + * + * @return false|string The formatted value or false on error + * + * @see https://php.net/numberformatter.format + * + * @throws NotImplementedException If the method is called with the class $style 'CURRENCY' + * @throws MethodArgumentValueNotImplementedException If the $type is different than TYPE_DEFAULT + */ + public function format($num, int $type = self::TYPE_DEFAULT) + { + // The original NumberFormatter does not support this format type + if (self::TYPE_CURRENCY === $type) { + if (\PHP_VERSION_ID >= 80000) { + throw new \ValueError(sprintf('The format type must be a NumberFormatter::TYPE_* constant (%s given).', $type)); + } + + trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING); + + return false; + } + + if (self::CURRENCY === $this->style) { + throw new NotImplementedException(sprintf('"%s()" method does not support the formatting of currencies (instance with CURRENCY style). "%s".', __METHOD__, NotImplementedException::INTL_INSTALL_MESSAGE)); + } + + // Only the default type is supported. + if (self::TYPE_DEFAULT !== $type) { + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'type', $type, 'Only TYPE_DEFAULT is supported'); + } + + $fractionDigits = $this->getAttribute(self::FRACTION_DIGITS); + + $num = $this->round($num, $fractionDigits); + $num = $this->formatNumber($num, $fractionDigits); + + // behave like the intl extension + $this->resetError(); + + return $num; + } + + /** + * Returns an attribute value. + * + * @return int|false The attribute value on success or false on error + * + * @see https://php.net/numberformatter.getattribute + */ + public function getAttribute(int $attribute) + { + return $this->attributes[$attribute] ?? null; + } + + /** + * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value. + * + * @return int The error code from last formatter call + * + * @see https://php.net/numberformatter.geterrorcode + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value. + * + * @return string The error message from last formatter call + * + * @see https://php.net/numberformatter.geterrormessage + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Returns the formatter's locale. + * + * The parameter $type is currently ignored. + * + * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE) + * + * @return string The locale used to create the formatter. Currently always + * returns "en". + * + * @see https://php.net/numberformatter.getlocale + */ + public function getLocale(int $type = Locale::ACTUAL_LOCALE) + { + return 'en'; + } + + /** + * Not supported. Returns the formatter's pattern. + * + * @return string|false The pattern string used by the formatter or false on error + * + * @see https://php.net/numberformatter.getpattern + * + * @throws MethodNotImplementedException + */ + public function getPattern() + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Returns a formatter symbol value. + * + * @return string|false The symbol value or false on error + * + * @see https://php.net/numberformatter.getsymbol + */ + public function getSymbol(int $symbol) + { + return \array_key_exists($this->style, self::$enSymbols) && \array_key_exists($symbol, self::$enSymbols[$this->style]) ? self::$enSymbols[$this->style][$symbol] : false; + } + + /** + * Not supported. Returns a formatter text attribute value. + * + * @return string|false The attribute value or false on error + * + * @see https://php.net/numberformatter.gettextattribute + */ + public function getTextAttribute(int $attribute) + { + return \array_key_exists($this->style, self::$enTextAttributes) && \array_key_exists($attribute, self::$enTextAttributes[$this->style]) ? self::$enTextAttributes[$this->style][$attribute] : false; + } + + /** + * Not supported. Parse a currency number. + * + * @return float|false The parsed numeric value or false on error + * + * @see https://php.net/numberformatter.parsecurrency + * + * @throws MethodNotImplementedException + */ + public function parseCurrency(string $string, &$currency, &$offset = null) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Parse a number. + * + * @return int|float|false The parsed value or false on error + * + * @see https://php.net/numberformatter.parse + */ + public function parse(string $string, int $type = self::TYPE_DOUBLE, &$offset = null) + { + if (self::TYPE_DEFAULT === $type || self::TYPE_CURRENCY === $type) { + if (\PHP_VERSION_ID >= 80000) { + throw new \ValueError(sprintf('The format type must be a NumberFormatter::TYPE_* constant (%d given).', $type)); + } + + trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING); + + return false; + } + + // Any invalid number at the end of the string is removed. + // Only numbers and the fraction separator is expected in the string. + // If grouping is used, grouping separator also becomes a valid character. + $groupingMatch = $this->getAttribute(self::GROUPING_USED) ? '|(?P\d++(,{1}\d+)++(\.\d*+)?)' : ''; + if (preg_match("/^-?(?:\.\d++{$groupingMatch}|\d++(\.\d*+)?)/", $string, $matches)) { + $string = $matches[0]; + $offset = \strlen($string); + // value is not valid if grouping is used, but digits are not grouped in groups of three + if ($error = isset($matches['grouping']) && !preg_match('/^-?(?:\d{1,3}+)?(?:(?:,\d{3})++|\d*+)(?:\.\d*+)?$/', $string)) { + // the position on error is 0 for positive and 1 for negative numbers + $offset = 0 === strpos($string, '-') ? 1 : 0; + } + } else { + $error = true; + $offset = 0; + } + + if ($error) { + Icu::setError(Icu::U_PARSE_ERROR, 'Number parsing failed'); + $this->errorCode = Icu::getErrorCode(); + $this->errorMessage = Icu::getErrorMessage(); + + return false; + } + + $string = str_replace(',', '', $string); + $string = $this->convertValueDataType($string, $type); + + // behave like the intl extension + $this->resetError(); + + return $string; + } + + /** + * Set an attribute. + * + * @param int|float $value + * + * @return bool true on success or false on failure + * + * @see https://php.net/numberformatter.setattribute + * + * @throws MethodArgumentValueNotImplementedException When the $attribute is not supported + * @throws MethodArgumentValueNotImplementedException When the $value is not supported + */ + public function setAttribute(int $attribute, $value) + { + if (!\in_array($attribute, self::$supportedAttributes)) { + $message = sprintf( + 'The available attributes are: %s', + implode(', ', array_keys(self::$supportedAttributes)) + ); + + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attribute', $value, $message); + } + + if (self::$supportedAttributes['ROUNDING_MODE'] === $attribute && $this->isInvalidRoundingMode($value)) { + $message = sprintf( + 'The supported values for ROUNDING_MODE are: %s', + implode(', ', array_keys(self::$roundingModes)) + ); + + throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attribute', $value, $message); + } + + if (self::$supportedAttributes['GROUPING_USED'] === $attribute) { + $value = $this->normalizeGroupingUsedValue($value); + } + + if (self::$supportedAttributes['FRACTION_DIGITS'] === $attribute) { + $value = $this->normalizeFractionDigitsValue($value); + if ($value < 0) { + // ignore negative values but do not raise an error + return true; + } + } + + $this->attributes[$attribute] = $value; + $this->initializedAttributes[$attribute] = true; + + return true; + } + + /** + * Not supported. Set the formatter's pattern. + * + * @return bool true on success or false on failure + * + * @see https://php.net/numberformatter.setpattern + * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details + * + * @throws MethodNotImplementedException + */ + public function setPattern(string $pattern) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Set the formatter's symbol. + * + * @return bool true on success or false on failure + * + * @see https://php.net/numberformatter.setsymbol + * + * @throws MethodNotImplementedException + */ + public function setSymbol(int $symbol, string $value) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Not supported. Set a text attribute. + * + * @return bool true on success or false on failure + * + * @see https://php.net/numberformatter.settextattribute + * + * @throws MethodNotImplementedException + */ + public function setTextAttribute(int $attribute, string $value) + { + throw new MethodNotImplementedException(__METHOD__); + } + + /** + * Set the error to the default U_ZERO_ERROR. + */ + protected function resetError() + { + Icu::setError(Icu::U_ZERO_ERROR); + $this->errorCode = Icu::getErrorCode(); + $this->errorMessage = Icu::getErrorMessage(); + } + + /** + * Rounds a currency value, applying increment rounding if applicable. + * + * When a currency have a rounding increment, an extra round is made after the first one. The rounding factor is + * determined in the ICU data and is explained as of: + * + * "the rounding increment is given in units of 10^(-fraction_digits)" + * + * The only actual rounding data as of this writing, is CHF. + * + * @see http://en.wikipedia.org/wiki/Swedish_rounding + * @see http://www.docjar.com/html/api/com/ibm/icu/util/Currency.java.html#1007 + */ + private function roundCurrency(float $value, string $currency): float + { + $fractionDigits = Currencies::getFractionDigits($currency); + $roundingIncrement = Currencies::getRoundingIncrement($currency); + + // Round with the formatter rounding mode + $value = $this->round($value, $fractionDigits); + + // Swiss rounding + if (0 < $roundingIncrement && 0 < $fractionDigits) { + $roundingFactor = $roundingIncrement / 10 ** $fractionDigits; + $value = round($value / $roundingFactor) * $roundingFactor; + } + + return $value; + } + + /** + * Rounds a value. + * + * @param int|float $value The value to round + * + * @return int|float The rounded value + */ + private function round($value, int $precision) + { + $precision = $this->getUninitializedPrecision($value, $precision); + + $roundingModeAttribute = $this->getAttribute(self::ROUNDING_MODE); + if (isset(self::$phpRoundingMap[$roundingModeAttribute])) { + $value = round($value, $precision, self::$phpRoundingMap[$roundingModeAttribute]); + } elseif (isset(self::$customRoundingList[$roundingModeAttribute])) { + $roundingCoef = 10 ** $precision; + $value *= $roundingCoef; + $value = (float) (string) $value; + + switch ($roundingModeAttribute) { + case self::ROUND_CEILING: + $value = ceil($value); + break; + case self::ROUND_FLOOR: + $value = floor($value); + break; + case self::ROUND_UP: + $value = $value > 0 ? ceil($value) : floor($value); + break; + case self::ROUND_DOWN: + $value = $value > 0 ? floor($value) : ceil($value); + break; + } + + $value /= $roundingCoef; + } + + return $value; + } + + /** + * Formats a number. + * + * @param int|float $value The numeric value to format + */ + private function formatNumber($value, int $precision): string + { + $precision = $this->getUninitializedPrecision($value, $precision); + + return number_format($value, $precision, '.', $this->getAttribute(self::GROUPING_USED) ? ',' : ''); + } + + /** + * Returns the precision value if the DECIMAL style is being used and the FRACTION_DIGITS attribute is uninitialized. + * + * @param int|float $value The value to get the precision from if the FRACTION_DIGITS attribute is uninitialized + */ + private function getUninitializedPrecision($value, int $precision): int + { + if (self::CURRENCY === $this->style) { + return $precision; + } + + if (!$this->isInitializedAttribute(self::FRACTION_DIGITS)) { + preg_match('/.*\.(.*)/', (string) $value, $digits); + if (isset($digits[1])) { + $precision = \strlen($digits[1]); + } + } + + return $precision; + } + + /** + * Check if the attribute is initialized (value set by client code). + */ + private function isInitializedAttribute(string $attr): bool + { + return isset($this->initializedAttributes[$attr]); + } + + /** + * Returns the numeric value using the $type to convert to the right data type. + * + * @param mixed $value The value to be converted + * + * @return int|float|false The converted value + */ + private function convertValueDataType($value, int $type) + { + if (self::TYPE_DOUBLE === $type) { + $value = (float) $value; + } elseif (self::TYPE_INT32 === $type) { + $value = $this->getInt32Value($value); + } elseif (self::TYPE_INT64 === $type) { + $value = $this->getInt64Value($value); + } + + return $value; + } + + /** + * Convert the value data type to int or returns false if the value is out of the integer value range. + * + * @return int|false The converted value + */ + private function getInt32Value($value) + { + if ($value > self::$int32Max || $value < -self::$int32Max - 1) { + return false; + } + + return (int) $value; + } + + /** + * Convert the value data type to int or returns false if the value is out of the integer value range. + * + * @return int|float|false The converted value + */ + private function getInt64Value($value) + { + if ($value > self::$int64Max || $value < -self::$int64Max - 1) { + return false; + } + + if (\PHP_INT_SIZE !== 8 && ($value > self::$int32Max || $value < -self::$int32Max - 1)) { + return (float) $value; + } + + return (int) $value; + } + + /** + * Check if the rounding mode is invalid. + */ + private function isInvalidRoundingMode(int $value): bool + { + if (\in_array($value, self::$roundingModes, true)) { + return false; + } + + return true; + } + + /** + * Returns the normalized value for the GROUPING_USED attribute. Any value that can be converted to int will be + * cast to Boolean and then to int again. This way, negative values are converted to 1 and string values to 0. + */ + private function normalizeGroupingUsedValue($value): int + { + return (int) (bool) (int) $value; + } + + /** + * Returns the normalized value for the FRACTION_DIGITS attribute. + */ + private function normalizeFractionDigitsValue($value): int + { + return (int) $value; + } +} diff --git a/lib/symfony/polyfill-intl-icu/README.md b/lib/symfony/polyfill-intl-icu/README.md new file mode 100644 index 0000000000..b7faedc5d2 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/README.md @@ -0,0 +1,23 @@ +Symfony Polyfill / Intl: ICU +============================ + +This package provides fallback implementations when the +[Intl](https://php.net/intl) extension is not installed. +It is limited to the "en" locale and to: + +- [`intl_is_failure()`](https://php.net/intl-is-failure) +- [`intl_get_error_code()`](https://php.net/intl-get-error-code) +- [`intl_get_error_message()`](https://php.net/intl-get-error-message) +- [`intl_error_name()`](https://php.net/intl-error-name) +- [`Collator`](https://php.net/Collator) +- [`NumberFormatter`](https://php.net/NumberFormatter) +- [`Locale`](https://php.net/Locale) +- [`IntlDateFormatter`](https://php.net/IntlDateFormatter) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/lib/symfony/polyfill-intl-icu/Resources/currencies.php b/lib/symfony/polyfill-intl-icu/Resources/currencies.php new file mode 100644 index 0000000000..3ed9b47f1f --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Resources/currencies.php @@ -0,0 +1,1329 @@ + + array ( + 0 => 'ADP', + 1 => 0, + 2 => 0, + ), + 'AED' => + array ( + 0 => 'AED', + ), + 'AFA' => + array ( + 0 => 'AFA', + ), + 'AFN' => + array ( + 0 => 'AFN', + 1 => 0, + 2 => 0, + ), + 'ALK' => + array ( + 0 => 'ALK', + ), + 'ALL' => + array ( + 0 => 'ALL', + 1 => 0, + 2 => 0, + ), + 'AMD' => + array ( + 0 => 'AMD', + 1 => 2, + 2 => 0, + ), + 'ANG' => + array ( + 0 => 'ANG', + ), + 'AOA' => + array ( + 0 => 'AOA', + ), + 'AOK' => + array ( + 0 => 'AOK', + ), + 'AON' => + array ( + 0 => 'AON', + ), + 'AOR' => + array ( + 0 => 'AOR', + ), + 'ARA' => + array ( + 0 => 'ARA', + ), + 'ARL' => + array ( + 0 => 'ARL', + ), + 'ARM' => + array ( + 0 => 'ARM', + ), + 'ARP' => + array ( + 0 => 'ARP', + ), + 'ARS' => + array ( + 0 => 'ARS', + ), + 'ATS' => + array ( + 0 => 'ATS', + ), + 'AUD' => + array ( + 0 => 'A$', + ), + 'AWG' => + array ( + 0 => 'AWG', + ), + 'AZM' => + array ( + 0 => 'AZM', + ), + 'AZN' => + array ( + 0 => 'AZN', + ), + 'BAD' => + array ( + 0 => 'BAD', + ), + 'BAM' => + array ( + 0 => 'BAM', + ), + 'BAN' => + array ( + 0 => 'BAN', + ), + 'BBD' => + array ( + 0 => 'BBD', + ), + 'BDT' => + array ( + 0 => 'BDT', + ), + 'BEC' => + array ( + 0 => 'BEC', + ), + 'BEF' => + array ( + 0 => 'BEF', + ), + 'BEL' => + array ( + 0 => 'BEL', + ), + 'BGL' => + array ( + 0 => 'BGL', + ), + 'BGM' => + array ( + 0 => 'BGM', + ), + 'BGN' => + array ( + 0 => 'BGN', + ), + 'BGO' => + array ( + 0 => 'BGO', + ), + 'BHD' => + array ( + 0 => 'BHD', + 1 => 3, + 2 => 0, + ), + 'BIF' => + array ( + 0 => 'BIF', + 1 => 0, + 2 => 0, + ), + 'BMD' => + array ( + 0 => 'BMD', + ), + 'BND' => + array ( + 0 => 'BND', + ), + 'BOB' => + array ( + 0 => 'BOB', + ), + 'BOL' => + array ( + 0 => 'BOL', + ), + 'BOP' => + array ( + 0 => 'BOP', + ), + 'BOV' => + array ( + 0 => 'BOV', + ), + 'BRB' => + array ( + 0 => 'BRB', + ), + 'BRC' => + array ( + 0 => 'BRC', + ), + 'BRE' => + array ( + 0 => 'BRE', + ), + 'BRL' => + array ( + 0 => 'R$', + ), + 'BRN' => + array ( + 0 => 'BRN', + ), + 'BRR' => + array ( + 0 => 'BRR', + ), + 'BRZ' => + array ( + 0 => 'BRZ', + ), + 'BSD' => + array ( + 0 => 'BSD', + ), + 'BTN' => + array ( + 0 => 'BTN', + ), + 'BUK' => + array ( + 0 => 'BUK', + ), + 'BWP' => + array ( + 0 => 'BWP', + ), + 'BYB' => + array ( + 0 => 'BYB', + ), + 'BYN' => + array ( + 0 => 'BYN', + 1 => 2, + 2 => 0, + ), + 'BYR' => + array ( + 0 => 'BYR', + 1 => 0, + 2 => 0, + ), + 'BZD' => + array ( + 0 => 'BZD', + ), + 'CAD' => + array ( + 0 => 'CA$', + 1 => 2, + 2 => 0, + ), + 'CDF' => + array ( + 0 => 'CDF', + ), + 'CHE' => + array ( + 0 => 'CHE', + ), + 'CHF' => + array ( + 0 => 'CHF', + 1 => 2, + 2 => 0, + ), + 'CHW' => + array ( + 0 => 'CHW', + ), + 'CLE' => + array ( + 0 => 'CLE', + ), + 'CLF' => + array ( + 0 => 'CLF', + 1 => 4, + 2 => 0, + ), + 'CLP' => + array ( + 0 => 'CLP', + 1 => 0, + 2 => 0, + ), + 'CNH' => + array ( + 0 => 'CNH', + ), + 'CNX' => + array ( + 0 => 'CNX', + ), + 'CNY' => + array ( + 0 => 'CN¥', + ), + 'COP' => + array ( + 0 => 'COP', + 1 => 2, + 2 => 0, + ), + 'COU' => + array ( + 0 => 'COU', + ), + 'CRC' => + array ( + 0 => 'CRC', + 1 => 2, + 2 => 0, + ), + 'CSD' => + array ( + 0 => 'CSD', + ), + 'CSK' => + array ( + 0 => 'CSK', + ), + 'CUC' => + array ( + 0 => 'CUC', + ), + 'CUP' => + array ( + 0 => 'CUP', + ), + 'CVE' => + array ( + 0 => 'CVE', + ), + 'CYP' => + array ( + 0 => 'CYP', + ), + 'CZK' => + array ( + 0 => 'CZK', + 1 => 2, + 2 => 0, + ), + 'DDM' => + array ( + 0 => 'DDM', + ), + 'DEM' => + array ( + 0 => 'DEM', + ), + 'DJF' => + array ( + 0 => 'DJF', + 1 => 0, + 2 => 0, + ), + 'DKK' => + array ( + 0 => 'DKK', + 1 => 2, + 2 => 0, + ), + 'DOP' => + array ( + 0 => 'DOP', + ), + 'DZD' => + array ( + 0 => 'DZD', + ), + 'ECS' => + array ( + 0 => 'ECS', + ), + 'ECV' => + array ( + 0 => 'ECV', + ), + 'EEK' => + array ( + 0 => 'EEK', + ), + 'EGP' => + array ( + 0 => 'EGP', + ), + 'ERN' => + array ( + 0 => 'ERN', + ), + 'ESA' => + array ( + 0 => 'ESA', + ), + 'ESB' => + array ( + 0 => 'ESB', + ), + 'ESP' => + array ( + 0 => 'ESP', + 1 => 0, + 2 => 0, + ), + 'ETB' => + array ( + 0 => 'ETB', + ), + 'EUR' => + array ( + 0 => '€', + ), + 'FIM' => + array ( + 0 => 'FIM', + ), + 'FJD' => + array ( + 0 => 'FJD', + ), + 'FKP' => + array ( + 0 => 'FKP', + ), + 'FRF' => + array ( + 0 => 'FRF', + ), + 'GBP' => + array ( + 0 => '£', + ), + 'GEK' => + array ( + 0 => 'GEK', + ), + 'GEL' => + array ( + 0 => 'GEL', + ), + 'GHC' => + array ( + 0 => 'GHC', + ), + 'GHS' => + array ( + 0 => 'GHS', + ), + 'GIP' => + array ( + 0 => 'GIP', + ), + 'GMD' => + array ( + 0 => 'GMD', + ), + 'GNF' => + array ( + 0 => 'GNF', + 1 => 0, + 2 => 0, + ), + 'GNS' => + array ( + 0 => 'GNS', + ), + 'GQE' => + array ( + 0 => 'GQE', + ), + 'GRD' => + array ( + 0 => 'GRD', + ), + 'GTQ' => + array ( + 0 => 'GTQ', + ), + 'GWE' => + array ( + 0 => 'GWE', + ), + 'GWP' => + array ( + 0 => 'GWP', + ), + 'GYD' => + array ( + 0 => 'GYD', + 1 => 2, + 2 => 0, + ), + 'HKD' => + array ( + 0 => 'HK$', + ), + 'HNL' => + array ( + 0 => 'HNL', + ), + 'HRD' => + array ( + 0 => 'HRD', + ), + 'HRK' => + array ( + 0 => 'HRK', + ), + 'HTG' => + array ( + 0 => 'HTG', + ), + 'HUF' => + array ( + 0 => 'HUF', + 1 => 2, + 2 => 0, + ), + 'IDR' => + array ( + 0 => 'IDR', + 1 => 2, + 2 => 0, + ), + 'IEP' => + array ( + 0 => 'IEP', + ), + 'ILP' => + array ( + 0 => 'ILP', + ), + 'ILR' => + array ( + 0 => 'ILR', + ), + 'ILS' => + array ( + 0 => '₪', + ), + 'INR' => + array ( + 0 => '₹', + ), + 'IQD' => + array ( + 0 => 'IQD', + 1 => 0, + 2 => 0, + ), + 'IRR' => + array ( + 0 => 'IRR', + 1 => 0, + 2 => 0, + ), + 'ISJ' => + array ( + 0 => 'ISJ', + ), + 'ISK' => + array ( + 0 => 'ISK', + 1 => 0, + 2 => 0, + ), + 'ITL' => + array ( + 0 => 'ITL', + 1 => 0, + 2 => 0, + ), + 'JMD' => + array ( + 0 => 'JMD', + ), + 'JOD' => + array ( + 0 => 'JOD', + 1 => 3, + 2 => 0, + ), + 'JPY' => + array ( + 0 => '¥', + 1 => 0, + 2 => 0, + ), + 'KES' => + array ( + 0 => 'KES', + ), + 'KGS' => + array ( + 0 => 'KGS', + ), + 'KHR' => + array ( + 0 => 'KHR', + ), + 'KMF' => + array ( + 0 => 'KMF', + 1 => 0, + 2 => 0, + ), + 'KPW' => + array ( + 0 => 'KPW', + 1 => 0, + 2 => 0, + ), + 'KRH' => + array ( + 0 => 'KRH', + ), + 'KRO' => + array ( + 0 => 'KRO', + ), + 'KRW' => + array ( + 0 => '₩', + 1 => 0, + 2 => 0, + ), + 'KWD' => + array ( + 0 => 'KWD', + 1 => 3, + 2 => 0, + ), + 'KYD' => + array ( + 0 => 'KYD', + ), + 'KZT' => + array ( + 0 => 'KZT', + ), + 'LAK' => + array ( + 0 => 'LAK', + 1 => 0, + 2 => 0, + ), + 'LBP' => + array ( + 0 => 'LBP', + 1 => 0, + 2 => 0, + ), + 'LKR' => + array ( + 0 => 'LKR', + ), + 'LRD' => + array ( + 0 => 'LRD', + ), + 'LSL' => + array ( + 0 => 'LSL', + ), + 'LTL' => + array ( + 0 => 'LTL', + ), + 'LTT' => + array ( + 0 => 'LTT', + ), + 'LUC' => + array ( + 0 => 'LUC', + ), + 'LUF' => + array ( + 0 => 'LUF', + 1 => 0, + 2 => 0, + ), + 'LUL' => + array ( + 0 => 'LUL', + ), + 'LVL' => + array ( + 0 => 'LVL', + ), + 'LVR' => + array ( + 0 => 'LVR', + ), + 'LYD' => + array ( + 0 => 'LYD', + 1 => 3, + 2 => 0, + ), + 'MAD' => + array ( + 0 => 'MAD', + ), + 'MAF' => + array ( + 0 => 'MAF', + ), + 'MCF' => + array ( + 0 => 'MCF', + ), + 'MDC' => + array ( + 0 => 'MDC', + ), + 'MDL' => + array ( + 0 => 'MDL', + ), + 'MGA' => + array ( + 0 => 'MGA', + 1 => 0, + 2 => 0, + ), + 'MGF' => + array ( + 0 => 'MGF', + 1 => 0, + 2 => 0, + ), + 'MKD' => + array ( + 0 => 'MKD', + ), + 'MKN' => + array ( + 0 => 'MKN', + ), + 'MLF' => + array ( + 0 => 'MLF', + ), + 'MMK' => + array ( + 0 => 'MMK', + 1 => 0, + 2 => 0, + ), + 'MNT' => + array ( + 0 => 'MNT', + 1 => 2, + 2 => 0, + ), + 'MOP' => + array ( + 0 => 'MOP', + ), + 'MRO' => + array ( + 0 => 'MRO', + 1 => 0, + 2 => 0, + ), + 'MRU' => + array ( + 0 => 'MRU', + ), + 'MTL' => + array ( + 0 => 'MTL', + ), + 'MTP' => + array ( + 0 => 'MTP', + ), + 'MUR' => + array ( + 0 => 'MUR', + 1 => 2, + 2 => 0, + ), + 'MVP' => + array ( + 0 => 'MVP', + ), + 'MVR' => + array ( + 0 => 'MVR', + ), + 'MWK' => + array ( + 0 => 'MWK', + ), + 'MXN' => + array ( + 0 => 'MX$', + ), + 'MXP' => + array ( + 0 => 'MXP', + ), + 'MXV' => + array ( + 0 => 'MXV', + ), + 'MYR' => + array ( + 0 => 'MYR', + ), + 'MZE' => + array ( + 0 => 'MZE', + ), + 'MZM' => + array ( + 0 => 'MZM', + ), + 'MZN' => + array ( + 0 => 'MZN', + ), + 'NAD' => + array ( + 0 => 'NAD', + ), + 'NGN' => + array ( + 0 => 'NGN', + ), + 'NIC' => + array ( + 0 => 'NIC', + ), + 'NIO' => + array ( + 0 => 'NIO', + ), + 'NLG' => + array ( + 0 => 'NLG', + ), + 'NOK' => + array ( + 0 => 'NOK', + 1 => 2, + 2 => 0, + ), + 'NPR' => + array ( + 0 => 'NPR', + ), + 'NZD' => + array ( + 0 => 'NZ$', + ), + 'OMR' => + array ( + 0 => 'OMR', + 1 => 3, + 2 => 0, + ), + 'PAB' => + array ( + 0 => 'PAB', + ), + 'PEI' => + array ( + 0 => 'PEI', + ), + 'PEN' => + array ( + 0 => 'PEN', + ), + 'PES' => + array ( + 0 => 'PES', + ), + 'PGK' => + array ( + 0 => 'PGK', + ), + 'PHP' => + array ( + 0 => '₱', + ), + 'PKR' => + array ( + 0 => 'PKR', + 1 => 2, + 2 => 0, + ), + 'PLN' => + array ( + 0 => 'PLN', + ), + 'PLZ' => + array ( + 0 => 'PLZ', + ), + 'PTE' => + array ( + 0 => 'PTE', + ), + 'PYG' => + array ( + 0 => 'PYG', + 1 => 0, + 2 => 0, + ), + 'QAR' => + array ( + 0 => 'QAR', + ), + 'RHD' => + array ( + 0 => 'RHD', + ), + 'ROL' => + array ( + 0 => 'ROL', + ), + 'RON' => + array ( + 0 => 'RON', + ), + 'RSD' => + array ( + 0 => 'RSD', + 1 => 0, + 2 => 0, + ), + 'RUB' => + array ( + 0 => 'RUB', + ), + 'RUR' => + array ( + 0 => 'RUR', + ), + 'RWF' => + array ( + 0 => 'RWF', + 1 => 0, + 2 => 0, + ), + 'SAR' => + array ( + 0 => 'SAR', + ), + 'SBD' => + array ( + 0 => 'SBD', + ), + 'SCR' => + array ( + 0 => 'SCR', + ), + 'SDD' => + array ( + 0 => 'SDD', + ), + 'SDG' => + array ( + 0 => 'SDG', + ), + 'SDP' => + array ( + 0 => 'SDP', + ), + 'SEK' => + array ( + 0 => 'SEK', + 1 => 2, + 2 => 0, + ), + 'SGD' => + array ( + 0 => 'SGD', + ), + 'SHP' => + array ( + 0 => 'SHP', + ), + 'SIT' => + array ( + 0 => 'SIT', + ), + 'SKK' => + array ( + 0 => 'SKK', + ), + 'SLE' => + array ( + 0 => 'SLE', + 1 => 2, + 2 => 0, + ), + 'SLL' => + array ( + 0 => 'SLL', + 1 => 0, + 2 => 0, + ), + 'SOS' => + array ( + 0 => 'SOS', + 1 => 0, + 2 => 0, + ), + 'SRD' => + array ( + 0 => 'SRD', + ), + 'SRG' => + array ( + 0 => 'SRG', + ), + 'SSP' => + array ( + 0 => 'SSP', + ), + 'STD' => + array ( + 0 => 'STD', + 1 => 0, + 2 => 0, + ), + 'STN' => + array ( + 0 => 'STN', + ), + 'SUR' => + array ( + 0 => 'SUR', + ), + 'SVC' => + array ( + 0 => 'SVC', + ), + 'SYP' => + array ( + 0 => 'SYP', + 1 => 0, + 2 => 0, + ), + 'SZL' => + array ( + 0 => 'SZL', + ), + 'THB' => + array ( + 0 => 'THB', + ), + 'TJR' => + array ( + 0 => 'TJR', + ), + 'TJS' => + array ( + 0 => 'TJS', + ), + 'TMM' => + array ( + 0 => 'TMM', + 1 => 0, + 2 => 0, + ), + 'TMT' => + array ( + 0 => 'TMT', + ), + 'TND' => + array ( + 0 => 'TND', + 1 => 3, + 2 => 0, + ), + 'TOP' => + array ( + 0 => 'TOP', + ), + 'TPE' => + array ( + 0 => 'TPE', + ), + 'TRL' => + array ( + 0 => 'TRL', + 1 => 0, + 2 => 0, + ), + 'TRY' => + array ( + 0 => 'TRY', + ), + 'TTD' => + array ( + 0 => 'TTD', + ), + 'TWD' => + array ( + 0 => 'NT$', + 1 => 2, + 2 => 0, + ), + 'TZS' => + array ( + 0 => 'TZS', + 1 => 2, + 2 => 0, + ), + 'UAH' => + array ( + 0 => 'UAH', + ), + 'UAK' => + array ( + 0 => 'UAK', + ), + 'UGS' => + array ( + 0 => 'UGS', + ), + 'UGX' => + array ( + 0 => 'UGX', + 1 => 0, + 2 => 0, + ), + 'USD' => + array ( + 0 => '$', + ), + 'USN' => + array ( + 0 => 'USN', + ), + 'USS' => + array ( + 0 => 'USS', + ), + 'UYI' => + array ( + 0 => 'UYI', + 1 => 0, + 2 => 0, + ), + 'UYP' => + array ( + 0 => 'UYP', + ), + 'UYU' => + array ( + 0 => 'UYU', + ), + 'UYW' => + array ( + 0 => 'UYW', + 1 => 4, + 2 => 0, + ), + 'UZS' => + array ( + 0 => 'UZS', + 1 => 2, + 2 => 0, + ), + 'VEB' => + array ( + 0 => 'VEB', + ), + 'VED' => + array ( + 0 => 'VED', + ), + 'VEF' => + array ( + 0 => 'VEF', + 1 => 2, + 2 => 0, + ), + 'VES' => + array ( + 0 => 'VES', + ), + 'VND' => + array ( + 0 => '₫', + 1 => 0, + 2 => 0, + ), + 'VNN' => + array ( + 0 => 'VNN', + ), + 'VUV' => + array ( + 0 => 'VUV', + 1 => 0, + 2 => 0, + ), + 'WST' => + array ( + 0 => 'WST', + ), + 'XAF' => + array ( + 0 => 'FCFA', + 1 => 0, + 2 => 0, + ), + 'XCD' => + array ( + 0 => 'EC$', + ), + 'XCG' => + array ( + 0 => 'Cg.', + ), + 'XEU' => + array ( + 0 => 'XEU', + ), + 'XFO' => + array ( + 0 => 'XFO', + ), + 'XFU' => + array ( + 0 => 'XFU', + ), + 'XOF' => + array ( + 0 => 'F CFA', + 1 => 0, + 2 => 0, + ), + 'XPF' => + array ( + 0 => 'CFPF', + 1 => 0, + 2 => 0, + ), + 'XRE' => + array ( + 0 => 'XRE', + ), + 'YDD' => + array ( + 0 => 'YDD', + ), + 'YER' => + array ( + 0 => 'YER', + 1 => 0, + 2 => 0, + ), + 'YUD' => + array ( + 0 => 'YUD', + ), + 'YUM' => + array ( + 0 => 'YUM', + ), + 'YUN' => + array ( + 0 => 'YUN', + ), + 'YUR' => + array ( + 0 => 'YUR', + ), + 'ZAL' => + array ( + 0 => 'ZAL', + ), + 'ZAR' => + array ( + 0 => 'ZAR', + ), + 'ZMK' => + array ( + 0 => 'ZMK', + 1 => 0, + 2 => 0, + ), + 'ZMW' => + array ( + 0 => 'ZMW', + ), + 'ZRN' => + array ( + 0 => 'ZRN', + ), + 'ZRZ' => + array ( + 0 => 'ZRZ', + ), + 'ZWD' => + array ( + 0 => 'ZWD', + 1 => 0, + 2 => 0, + ), + 'ZWG' => + array ( + 0 => 'ZWG', + ), + 'ZWL' => + array ( + 0 => 'ZWL', + ), + 'ZWR' => + array ( + 0 => 'ZWR', + ), + 'DEFAULT' => + array ( + 1 => 2, + 2 => 0, + ), +); diff --git a/lib/symfony/polyfill-intl-icu/Resources/stubs/Collator.php b/lib/symfony/polyfill-intl-icu/Resources/stubs/Collator.php new file mode 100644 index 0000000000..a1efbcb805 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Resources/stubs/Collator.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Icu\Collator as CollatorPolyfill; + +/** + * Stub implementation for the Collator class of the intl extension. + * + * @author Bernhard Schussek + */ +class Collator extends CollatorPolyfill +{ +} diff --git a/lib/symfony/polyfill-intl-icu/Resources/stubs/IntlDateFormatter.php b/lib/symfony/polyfill-intl-icu/Resources/stubs/IntlDateFormatter.php new file mode 100644 index 0000000000..e7012008e7 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Resources/stubs/IntlDateFormatter.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Icu\IntlDateFormatter as IntlDateFormatterPolyfill; + +/** + * Stub implementation for the IntlDateFormatter class of the intl extension. + * + * @author Bernhard Schussek + */ +class IntlDateFormatter extends IntlDateFormatterPolyfill +{ +} diff --git a/lib/symfony/polyfill-intl-icu/Resources/stubs/Locale.php b/lib/symfony/polyfill-intl-icu/Resources/stubs/Locale.php new file mode 100644 index 0000000000..f1b951e13a --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Resources/stubs/Locale.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Icu\Locale as LocalePolyfill; + +/** + * Stub implementation for the Locale class of the intl extension. + * + * @author Bernhard Schussek + */ +class Locale extends LocalePolyfill +{ +} diff --git a/lib/symfony/polyfill-intl-icu/Resources/stubs/NumberFormatter.php b/lib/symfony/polyfill-intl-icu/Resources/stubs/NumberFormatter.php new file mode 100644 index 0000000000..9288b9dd65 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/Resources/stubs/NumberFormatter.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Icu\NumberFormatter as NumberFormatterPolyfill; + +/** + * Stub implementation for the NumberFormatter class of the intl extension. + * + * @author Bernhard Schussek + * + * @see IntlNumberFormatter + */ +class NumberFormatter extends NumberFormatterPolyfill +{ +} diff --git a/lib/symfony/polyfill-intl-icu/bootstrap.php b/lib/symfony/polyfill-intl-icu/bootstrap.php new file mode 100644 index 0000000000..77d7543795 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/bootstrap.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Icu as p; + +if (extension_loaded('intl')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('intl_is_failure')) { + function intl_is_failure($errorCode) { return p\Icu::isFailure($errorCode); } +} +if (!function_exists('intl_get_error_code')) { + function intl_get_error_code() { return p\Icu::getErrorCode(); } +} +if (!function_exists('intl_get_error_message')) { + function intl_get_error_message() { return p\Icu::getErrorMessage(); } +} +if (!function_exists('intl_error_name')) { + function intl_error_name($errorCode) { return p\Icu::getErrorName($errorCode); } +} diff --git a/lib/symfony/polyfill-intl-icu/bootstrap80.php b/lib/symfony/polyfill-intl-icu/bootstrap80.php new file mode 100644 index 0000000000..ee1653a385 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/bootstrap80.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Icu as p; + +if (!function_exists('intl_is_failure')) { + function intl_is_failure(?int $errorCode): bool { return p\Icu::isFailure((int) $errorCode); } +} +if (!function_exists('intl_get_error_code')) { + function intl_get_error_code(): int { return p\Icu::getErrorCode(); } +} +if (!function_exists('intl_get_error_message')) { + function intl_get_error_message(): string { return p\Icu::getErrorMessage(); } +} +if (!function_exists('intl_error_name')) { + function intl_error_name(?int $errorCode): string { return p\Icu::getErrorName((int) $errorCode); } +} diff --git a/lib/symfony/polyfill-intl-icu/composer.json b/lib/symfony/polyfill-intl-icu/composer.json new file mode 100644 index 0000000000..33c74ab916 --- /dev/null +++ b/lib/symfony/polyfill-intl-icu/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/polyfill-intl-icu", + "type": "library", + "description": "Symfony polyfill for intl's ICU-related data and classes", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "icu"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "files": [ "bootstrap.php" ], + "psr-4": { "Symfony\\Polyfill\\Intl\\Icu\\": "" }, + "classmap": [ "Resources/stubs" ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/lib/symfony/property-access/CHANGELOG.md b/lib/symfony/property-access/CHANGELOG.md new file mode 100644 index 0000000000..a48ed823cc --- /dev/null +++ b/lib/symfony/property-access/CHANGELOG.md @@ -0,0 +1,88 @@ +CHANGELOG +========= + +6.3 +--- + + * Allow escaping `.` and `[` with `\` in `PropertyPath` + +6.2 +--- + + * Deprecate calling `PropertyAccessorBuilder::setCacheItemPool()` without arguments + * Added method `isNullSafe()` to `PropertyPathInterface`, implementing the interface without implementing this method + is deprecated + * Add support for the null-coalesce operator in property paths + +6.0 +--- + + * make `PropertyAccessor::__construct()` accept a combination of bitwise flags as first and second arguments + +5.3.0 +----- + + * deprecate passing a boolean as the second argument of `PropertyAccessor::__construct()`, expecting a combination of bitwise flags instead + +5.2.0 +----- + + * deprecated passing a boolean as the first argument of `PropertyAccessor::__construct()`, expecting a combination of bitwise flags instead + * added the ability to disable usage of the magic `__get` & `__set` methods + +5.1.0 +----- + + * Added an `UninitializedPropertyException` + * Linking to PropertyInfo extractor to remove a lot of duplicate code + +4.4.0 +----- + + * deprecated passing `null` as `$defaultLifetime` 2nd argument of `PropertyAccessor::createCache()` method, + pass `0` instead + +4.3.0 +----- + + * added a `$throwExceptionOnInvalidPropertyPath` argument to the PropertyAccessor constructor. + * added `enableExceptionOnInvalidPropertyPath()`, `disableExceptionOnInvalidPropertyPath()` and + `isExceptionOnInvalidPropertyPath()` methods to `PropertyAccessorBuilder` + +4.0.0 +----- + + * removed the `StringUtil` class, use `Symfony\Component\Inflector\Inflector` + +3.1.0 +----- + + * deprecated the `StringUtil` class, use `Symfony\Component\Inflector\Inflector` + instead + +2.7.0 +------ + + * `UnexpectedTypeException` now expects three constructor arguments: The invalid property value, + the `PropertyPathInterface` object and the current index of the property path. + +2.5.0 +------ + + * allowed non alpha numeric characters in second level and deeper object properties names + * [BC BREAK] when accessing an index on an object that does not implement + ArrayAccess, a NoSuchIndexException is now thrown instead of the + semantically wrong NoSuchPropertyException + * [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface + +2.3.0 +------ + + * added PropertyAccessorBuilder, to enable or disable the support of "__call" + * added support for "__call" in the PropertyAccessor (disabled by default) + * [BC BREAK] changed PropertyAccessor to continue its search for a property or + method even if a non-public match was found. Before, a PropertyAccessDeniedException + was thrown in this case. Class PropertyAccessDeniedException was removed + now. + * deprecated PropertyAccess::getPropertyAccessor + * added PropertyAccess::createPropertyAccessor and PropertyAccess::createPropertyAccessorBuilder diff --git a/lib/symfony/property-access/Exception/AccessException.php b/lib/symfony/property-access/Exception/AccessException.php new file mode 100644 index 0000000000..b3a854646e --- /dev/null +++ b/lib/symfony/property-access/Exception/AccessException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property path is not available. + * + * @author Stéphane Escandell + */ +class AccessException extends RuntimeException +{ +} diff --git a/lib/symfony/property-access/Exception/ExceptionInterface.php b/lib/symfony/property-access/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..fabf9a0802 --- /dev/null +++ b/lib/symfony/property-access/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Marker interface for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/lib/symfony/property-access/Exception/InvalidArgumentException.php b/lib/symfony/property-access/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..47bc7e150d --- /dev/null +++ b/lib/symfony/property-access/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base InvalidArgumentException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/lib/symfony/property-access/Exception/InvalidPropertyPathException.php b/lib/symfony/property-access/Exception/InvalidPropertyPathException.php new file mode 100644 index 0000000000..69de31cee4 --- /dev/null +++ b/lib/symfony/property-access/Exception/InvalidPropertyPathException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property path is malformed. + * + * @author Bernhard Schussek + */ +class InvalidPropertyPathException extends RuntimeException +{ +} diff --git a/lib/symfony/property-access/Exception/NoSuchIndexException.php b/lib/symfony/property-access/Exception/NoSuchIndexException.php new file mode 100644 index 0000000000..597b9904a2 --- /dev/null +++ b/lib/symfony/property-access/Exception/NoSuchIndexException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when an index cannot be found. + * + * @author Stéphane Escandell + */ +class NoSuchIndexException extends AccessException +{ +} diff --git a/lib/symfony/property-access/Exception/NoSuchPropertyException.php b/lib/symfony/property-access/Exception/NoSuchPropertyException.php new file mode 100644 index 0000000000..1c7eda5f83 --- /dev/null +++ b/lib/symfony/property-access/Exception/NoSuchPropertyException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property cannot be found. + * + * @author Bernhard Schussek + */ +class NoSuchPropertyException extends AccessException +{ +} diff --git a/lib/symfony/property-access/Exception/OutOfBoundsException.php b/lib/symfony/property-access/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000..a3c45597da --- /dev/null +++ b/lib/symfony/property-access/Exception/OutOfBoundsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base OutOfBoundsException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/lib/symfony/property-access/Exception/RuntimeException.php b/lib/symfony/property-access/Exception/RuntimeException.php new file mode 100644 index 0000000000..9fe843e309 --- /dev/null +++ b/lib/symfony/property-access/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base RuntimeException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/lib/symfony/property-access/Exception/UnexpectedTypeException.php b/lib/symfony/property-access/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000000..ed1308e0ba --- /dev/null +++ b/lib/symfony/property-access/Exception/UnexpectedTypeException.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\PropertyAccess\Exception; + +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * Thrown when a value does not match an expected type. + * + * @author Bernhard Schussek + */ +class UnexpectedTypeException extends RuntimeException +{ + /** + * @param mixed $value The unexpected value found while traversing property path + * @param int $pathIndex The property path index when the unexpected value was found + */ + public function __construct(mixed $value, PropertyPathInterface $path, int $pathIndex) + { + $message = \sprintf( + 'PropertyAccessor requires a graph of objects or arrays to operate on, '. + 'but it found type "%s" while trying to traverse path "%s" at property "%s".', + \gettype($value), + (string) $path, + $path->getElement($pathIndex) + ); + + parent::__construct($message); + } +} diff --git a/lib/symfony/property-access/Exception/UninitializedPropertyException.php b/lib/symfony/property-access/Exception/UninitializedPropertyException.php new file mode 100644 index 0000000000..c0d69735da --- /dev/null +++ b/lib/symfony/property-access/Exception/UninitializedPropertyException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property is not initialized. + * + * @author Jules Pietri + */ +class UninitializedPropertyException extends AccessException +{ +} diff --git a/lib/symfony/property-access/LICENSE b/lib/symfony/property-access/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/property-access/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/property-access/PropertyAccess.php b/lib/symfony/property-access/PropertyAccess.php new file mode 100644 index 0000000000..1953ac0963 --- /dev/null +++ b/lib/symfony/property-access/PropertyAccess.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Entry point of the PropertyAccess component. + * + * @author Bernhard Schussek + */ +final class PropertyAccess +{ + /** + * Creates a property accessor with the default configuration. + */ + public static function createPropertyAccessor(): PropertyAccessor + { + return self::createPropertyAccessorBuilder()->getPropertyAccessor(); + } + + public static function createPropertyAccessorBuilder(): PropertyAccessorBuilder + { + return new PropertyAccessorBuilder(); + } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } +} diff --git a/lib/symfony/property-access/PropertyAccessor.php b/lib/symfony/property-access/PropertyAccessor.php new file mode 100644 index 0000000000..1d96f1d3d3 --- /dev/null +++ b/lib/symfony/property-access/PropertyAccessor.php @@ -0,0 +1,715 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\PropertyAccess\Exception\AccessException; +use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; + +/** + * Default implementation of {@link PropertyAccessorInterface}. + * + * @author Bernhard Schussek + * @author Kévin Dunglas + * @author Nicolas Grekas + */ +class PropertyAccessor implements PropertyAccessorInterface +{ + /** @var int Allow none of the magic methods */ + public const DISALLOW_MAGIC_METHODS = ReflectionExtractor::DISALLOW_MAGIC_METHODS; + /** @var int Allow magic __get methods */ + public const MAGIC_GET = ReflectionExtractor::ALLOW_MAGIC_GET; + /** @var int Allow magic __set methods */ + public const MAGIC_SET = ReflectionExtractor::ALLOW_MAGIC_SET; + /** @var int Allow magic __call methods */ + public const MAGIC_CALL = ReflectionExtractor::ALLOW_MAGIC_CALL; + + public const DO_NOT_THROW = 0; + public const THROW_ON_INVALID_INDEX = 1; + public const THROW_ON_INVALID_PROPERTY_PATH = 2; + + private const VALUE = 0; + private const REF = 1; + private const IS_REF_CHAINED = 2; + private const CACHE_PREFIX_READ = 'r'; + private const CACHE_PREFIX_WRITE = 'w'; + private const CACHE_PREFIX_PROPERTY_PATH = 'p'; + private const RESULT_PROTO = [self::VALUE => null]; + + private int $magicMethodsFlags; + private bool $ignoreInvalidIndices; + private bool $ignoreInvalidProperty; + private ?CacheItemPoolInterface $cacheItemPool; + private array $propertyPathCache = []; + private PropertyReadInfoExtractorInterface $readInfoExtractor; + private PropertyWriteInfoExtractorInterface $writeInfoExtractor; + private array $readPropertyCache = []; + private array $writePropertyCache = []; + + /** + * Should not be used by application code. Use + * {@link PropertyAccess::createPropertyAccessor()} instead. + * + * @param int $magicMethods A bitwise combination of the MAGIC_* constants + * to specify the allowed magic methods (__get, __set, __call) + * or self::DISALLOW_MAGIC_METHODS for none + * @param int $throw A bitwise combination of the THROW_* constants + * to specify when exceptions should be thrown + */ + public function __construct(int $magicMethods = self::MAGIC_GET | self::MAGIC_SET, int $throw = self::THROW_ON_INVALID_PROPERTY_PATH, ?CacheItemPoolInterface $cacheItemPool = null, ?PropertyReadInfoExtractorInterface $readInfoExtractor = null, ?PropertyWriteInfoExtractorInterface $writeInfoExtractor = null) + { + $this->magicMethodsFlags = $magicMethods; + $this->ignoreInvalidIndices = 0 === ($throw & self::THROW_ON_INVALID_INDEX); + $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value + $this->ignoreInvalidProperty = 0 === ($throw & self::THROW_ON_INVALID_PROPERTY_PATH); + $this->readInfoExtractor = $readInfoExtractor ?? new ReflectionExtractor([], null, null, false); + $this->writeInfoExtractor = $writeInfoExtractor ?? new ReflectionExtractor(['set'], null, null, false); + } + + public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed + { + $zval = [ + self::VALUE => $objectOrArray, + ]; + + if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[?') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) { + return $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty)[self::VALUE]; + } + + $propertyPath = $this->getPropertyPath($propertyPath); + + $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); + + return $propertyValues[\count($propertyValues) - 1][self::VALUE]; + } + + /** + * @return void + */ + public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value) + { + if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) { + $zval = [ + self::VALUE => $objectOrArray, + ]; + + try { + $this->writeProperty($zval, $propertyPath, $value); + + return; + } catch (\TypeError $e) { + self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath, $e); + // It wasn't thrown in this class so rethrow it + throw $e; + } + } + + $propertyPath = $this->getPropertyPath($propertyPath); + + $zval = [ + self::VALUE => $objectOrArray, + self::REF => &$objectOrArray, + ]; + $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1); + $overwrite = true; + + try { + for ($i = \count($propertyValues) - 1; 0 <= $i; --$i) { + $zval = $propertyValues[$i]; + unset($propertyValues[$i]); + + // You only need set value for current element if: + // 1. it's the parent of the last index element + // OR + // 2. its child is not passed by reference + // + // This may avoid unnecessary value setting process for array elements. + // For example: + // '[a][b][c]' => 'old-value' + // If you want to change its value to 'new-value', + // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]' + if ($overwrite) { + $property = $propertyPath->getElement($i); + + if ($propertyPath->isIndex($i)) { + if ($overwrite = !isset($zval[self::REF])) { + $ref = &$zval[self::REF]; + $ref = $zval[self::VALUE]; + } + $this->writeIndex($zval, $property, $value); + if ($overwrite) { + $zval[self::VALUE] = $zval[self::REF]; + } + } else { + $this->writeProperty($zval, $property, $value); + } + + // if current element is an object + // OR + // if current element's reference chain is not broken - current element + // as well as all its ancients in the property path are all passed by reference, + // then there is no need to continue the value setting process + if (\is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) { + break; + } + } + + $value = $zval[self::VALUE]; + } + } catch (\TypeError $e) { + self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath, $e); + + // It wasn't thrown in this class so rethrow it + throw $e; + } + } + + private static function throwInvalidArgumentException(string $message, array $trace, int $i, string $propertyPath, ?\Throwable $previous = null): void + { + if (!isset($trace[$i]['file']) || __FILE__ !== $trace[$i]['file']) { + return; + } + if (preg_match('/^\S+::\S+\(\): Argument #\d+ \(\$\S+\) must be of type (\S+), (\S+) given/', $message, $matches)) { + [, $expectedType, $actualType] = $matches; + + throw new InvalidArgumentException(\sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 0, $previous); + } + if (preg_match('/^Cannot assign (\S+) to property \S+::\$\S+ of type (\S+)$/', $message, $matches)) { + [, $actualType, $expectedType] = $matches; + + throw new InvalidArgumentException(\sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 0, $previous); + } + } + + public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool + { + if (!$propertyPath instanceof PropertyPathInterface) { + $propertyPath = new PropertyPath($propertyPath); + } + + try { + $zval = [ + self::VALUE => $objectOrArray, + ]; + + // handle stdClass with properties with a dot in the name + if ($objectOrArray instanceof \stdClass && str_contains($propertyPath, '.') && property_exists($objectOrArray, $propertyPath)) { + $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty); + } else { + $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); + } + + return true; + } catch (AccessException) { + return false; + } catch (UnexpectedTypeException) { + return false; + } + } + + public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool + { + $propertyPath = $this->getPropertyPath($propertyPath); + + try { + $zval = [ + self::VALUE => $objectOrArray, + ]; + + // handle stdClass with properties with a dot in the name + if ($objectOrArray instanceof \stdClass && str_contains($propertyPath, '.') && property_exists($objectOrArray, $propertyPath)) { + $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty); + + return true; + } + + $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1); + + for ($i = \count($propertyValues) - 1; 0 <= $i; --$i) { + $zval = $propertyValues[$i]; + unset($propertyValues[$i]); + + if ($propertyPath->isIndex($i)) { + if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) { + return false; + } + } elseif (!\is_object($zval[self::VALUE]) || !$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) { + return false; + } + + if (\is_object($zval[self::VALUE])) { + return true; + } + } + + return true; + } catch (AccessException) { + return false; + } catch (UnexpectedTypeException) { + return false; + } + } + + /** + * Reads the path from an object up to a given path index. + * + * @throws UnexpectedTypeException if a value within the path is neither object nor array + * @throws NoSuchIndexException If a non-existing index is accessed + */ + private function readPropertiesUntil(array $zval, PropertyPathInterface $propertyPath, int $lastIndex, bool $ignoreInvalidIndices = true): array + { + if (!\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) { + throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0); + } + + // Add the root object to the list + $propertyValues = [$zval]; + + for ($i = 0; $i < $lastIndex; ++$i) { + $property = $propertyPath->getElement($i); + $isIndex = $propertyPath->isIndex($i); + + $isNullSafe = false; + if (method_exists($propertyPath, 'isNullSafe')) { + // To be removed in symfony 7 once we are sure isNullSafe is always implemented. + $isNullSafe = $propertyPath->isNullSafe($i); + } else { + trigger_deprecation('symfony/property-access', '6.2', 'The "%s()" method in class "%s" needs to be implemented in version 7.0, not defining it is deprecated.', 'isNullSafe', PropertyPathInterface::class); + } + + if ($isIndex) { + // Create missing nested arrays on demand + if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) + || (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property, $zval[self::VALUE])) + ) { + if (!$ignoreInvalidIndices && !$isNullSafe) { + if (!\is_array($zval[self::VALUE])) { + if (!$zval[self::VALUE] instanceof \Traversable) { + throw new NoSuchIndexException(\sprintf('Cannot read index "%s" while trying to traverse path "%s".', $property, (string) $propertyPath)); + } + + $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]); + } + + throw new NoSuchIndexException(\sprintf('Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".', $property, (string) $propertyPath, print_r(array_keys($zval[self::VALUE]), true))); + } + + if ($i + 1 < $propertyPath->getLength()) { + if (isset($zval[self::REF])) { + $zval[self::VALUE][$property] = []; + $zval[self::REF] = $zval[self::VALUE]; + } else { + $zval[self::VALUE] = [$property => []]; + } + } + } + + $zval = $this->readIndex($zval, $property); + } elseif ($isNullSafe && !\is_object($zval[self::VALUE])) { + $zval[self::VALUE] = null; + } else { + $zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty, $isNullSafe); + } + + // the final value of the path must not be validated + if ($i + 1 < $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE]) && !$isNullSafe) { + throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1); + } + + if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) { + // Set the IS_REF_CHAINED flag to true if: + // current property is passed by reference and + // it is the first element in the property path or + // the IS_REF_CHAINED flag of its parent element is true + // Basically, this flag is true only when the reference chain from the top element to current element is not broken + $zval[self::IS_REF_CHAINED] = true; + } + + $propertyValues[] = $zval; + + if ($isNullSafe && null === $zval[self::VALUE]) { + break; + } + } + + return $propertyValues; + } + + /** + * Reads a key from an array-like structure. + * + * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array + */ + private function readIndex(array $zval, string|int $index): array + { + if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) { + throw new NoSuchIndexException(\sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_debug_type($zval[self::VALUE]))); + } + + $result = self::RESULT_PROTO; + + if (isset($zval[self::VALUE][$index])) { + $result[self::VALUE] = $zval[self::VALUE][$index]; + + if (!isset($zval[self::REF])) { + // Save creating references when doing read-only lookups + } elseif (\is_array($zval[self::VALUE])) { + $result[self::REF] = &$zval[self::REF][$index]; + } elseif (\is_object($result[self::VALUE])) { + $result[self::REF] = $result[self::VALUE]; + } + } + + return $result; + } + + /** + * Reads the value of a property from an object. + * + * @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public + */ + private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false, bool $isNullSafe = false): array + { + if (!\is_object($zval[self::VALUE])) { + throw new NoSuchPropertyException(\sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property)); + } + + $result = self::RESULT_PROTO; + $object = $zval[self::VALUE]; + $class = $object::class; + $access = $this->getReadInfo($class, $property); + + if (null !== $access) { + $name = $access->getName(); + $type = $access->getType(); + + try { + if (PropertyReadInfo::TYPE_METHOD === $type) { + try { + $result[self::VALUE] = $object->$name(); + } catch (\TypeError $e) { + [$trace] = $e->getTrace(); + + // handle uninitialized properties in PHP >= 7 + if (__FILE__ === ($trace['file'] ?? null) + && $name === $trace['function'] + && $object instanceof $trace['class'] + && preg_match('/Return value (?:of .*::\w+\(\) )?must be of (?:the )?type (\w+), null returned$/', $e->getMessage(), $matches) + ) { + throw new UninitializedPropertyException(\sprintf('The method "%s::%s()" returned "null", but expected type "%3$s". Did you forget to initialize a property or to make the return type nullable using "?%3$s"?', get_debug_type($object), $name, $matches[1]), 0, $e); + } + + throw $e; + } + } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { + if (!isset($object->$name) && !\array_key_exists($name, (array) $object)) { + try { + $r = new \ReflectionProperty($class, $name); + + if ($r->isPublic() && !$r->hasType()) { + throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not initialized.', $class, $name)); + } + } catch (\ReflectionException $e) { + if (!$ignoreInvalidProperty) { + throw new NoSuchPropertyException(\sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); + } + } + } + + $result[self::VALUE] = $object->$name; + + if (isset($zval[self::REF]) && $access->canBeReference()) { + $result[self::REF] = &$object->$name; + } + } + } catch (\Error $e) { + // handle uninitialized properties in PHP >= 7.4 + if (preg_match('/^Typed property ([\w\\\\@]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches) || preg_match('/^Cannot access uninitialized non-nullable property ([\w\\\\@]+)::\$(\w+) by reference$/', $e->getMessage(), $matches)) { + $r = new \ReflectionProperty(str_contains($matches[1], '@anonymous') ? $class : $matches[1], $matches[2]); + $type = ($type = $r->getType()) instanceof \ReflectionNamedType ? $type->getName() : (string) $type; + + throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not readable because it is typed "%s". You should initialize it or declare a default value instead.', $matches[1], $r->getName(), $type), 0, $e); + } + + throw $e; + } + } elseif (property_exists($object, $property) && \array_key_exists($property, (array) $object)) { + $result[self::VALUE] = $object->$property; + if (isset($zval[self::REF])) { + $result[self::REF] = &$object->$property; + } + } elseif ($isNullSafe) { + $result[self::VALUE] = null; + } elseif (!$ignoreInvalidProperty) { + throw new NoSuchPropertyException(\sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); + } + + // Objects are always passed around by reference + if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) { + $result[self::REF] = $result[self::VALUE]; + } + + return $result; + } + + /** + * Guesses how to read the property value. + */ + private function getReadInfo(string $class, string $property): ?PropertyReadInfo + { + $key = str_replace('\\', '.', $class).'..'.$property; + + if (isset($this->readPropertyCache[$key])) { + return $this->readPropertyCache[$key]; + } + + if ($this->cacheItemPool) { + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.rawurlencode($key)); + if ($item->isHit()) { + return $this->readPropertyCache[$key] = $item->get(); + } + } + + $accessor = $this->readInfoExtractor->getReadInfo($class, $property, [ + 'enable_getter_setter_extraction' => true, + 'enable_magic_methods_extraction' => $this->magicMethodsFlags, + 'enable_constructor_extraction' => false, + ]); + + if (isset($item)) { + $this->cacheItemPool->save($item->set($accessor)); + } + + return $this->readPropertyCache[$key] = $accessor; + } + + /** + * Sets the value of an index in a given array-accessible value. + * + * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array + */ + private function writeIndex(array $zval, string|int $index, mixed $value): void + { + if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) { + throw new NoSuchIndexException(\sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_debug_type($zval[self::VALUE]))); + } + + $zval[self::REF][$index] = $value; + } + + /** + * Sets the value of a property in the given object. + * + * @throws NoSuchPropertyException if the property does not exist or is not public + */ + private function writeProperty(array $zval, string $property, mixed $value, bool $recursive = false): void + { + if (!\is_object($zval[self::VALUE])) { + throw new NoSuchPropertyException(\sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?', $property)); + } + + $object = $zval[self::VALUE]; + $class = $object::class; + $mutator = $this->getWriteInfo($class, $property, $value); + + try { + if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType()) { + $type = $mutator->getType(); + + if (PropertyWriteInfo::TYPE_METHOD === $type) { + $object->{$mutator->getName()}($value); + } elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) { + $object->{$mutator->getName()} = $value; + } elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) { + $this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo()); + } + } elseif ($object instanceof \stdClass && property_exists($object, $property)) { + $object->$property = $value; + } elseif (!$this->ignoreInvalidProperty) { + if ($mutator->hasErrors()) { + throw new NoSuchPropertyException(implode('. ', $mutator->getErrors()).'.'); + } + + throw new NoSuchPropertyException(\sprintf('Could not determine access type for property "%s" in class "%s".', $property, get_debug_type($object))); + } + } catch (\TypeError $e) { + if ($recursive || !$value instanceof \DateTimeInterface || !\in_array($value::class, ['DateTime', 'DateTimeImmutable'], true) || __FILE__ !== ($e->getTrace()[0]['file'] ?? null)) { + throw $e; + } + + $value = $value instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($value) : \DateTimeImmutable::createFromMutable($value); + try { + $this->writeProperty($zval, $property, $value, true); + } catch (\TypeError) { + throw $e; // throw the previous error + } + } + } + + /** + * Adjusts a collection-valued property by calling add*() and remove*() methods. + */ + private function writeCollection(array $zval, string $property, iterable $collection, PropertyWriteInfo $addMethod, PropertyWriteInfo $removeMethod): void + { + // At this point the add and remove methods have been found + $previousValue = $this->readProperty($zval, $property); + $previousValue = $previousValue[self::VALUE]; + + $removeMethodName = $removeMethod->getName(); + $addMethodName = $addMethod->getName(); + + if ($previousValue instanceof \Traversable) { + $previousValue = iterator_to_array($previousValue); + } + if ($previousValue && \is_array($previousValue)) { + if (\is_object($collection)) { + $collection = iterator_to_array($collection); + } + foreach ($previousValue as $key => $item) { + if (!\in_array($item, $collection, true)) { + unset($previousValue[$key]); + $zval[self::VALUE]->$removeMethodName($item); + } + } + } else { + $previousValue = false; + } + + foreach ($collection as $item) { + if (!$previousValue || !\in_array($item, $previousValue, true)) { + $zval[self::VALUE]->$addMethodName($item); + } + } + } + + private function getWriteInfo(string $class, string $property, mixed $value): PropertyWriteInfo + { + $useAdderAndRemover = is_iterable($value); + $key = str_replace('\\', '.', $class).'..'.$property.'..'.(int) $useAdderAndRemover; + + if (isset($this->writePropertyCache[$key])) { + return $this->writePropertyCache[$key]; + } + + if ($this->cacheItemPool) { + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.rawurlencode($key)); + if ($item->isHit()) { + return $this->writePropertyCache[$key] = $item->get(); + } + } + + $mutator = $this->writeInfoExtractor->getWriteInfo($class, $property, [ + 'enable_getter_setter_extraction' => true, + 'enable_magic_methods_extraction' => $this->magicMethodsFlags, + 'enable_constructor_extraction' => false, + 'enable_adder_remover_extraction' => $useAdderAndRemover, + ]); + + if (isset($item)) { + $this->cacheItemPool->save($item->set($mutator)); + } + + return $this->writePropertyCache[$key] = $mutator; + } + + /** + * Returns whether a property is writable in the given object. + */ + private function isPropertyWritable(object $object, string $property): bool + { + if ($object instanceof \stdClass && property_exists($object, $property)) { + return true; + } + + $mutatorForArray = $this->getWriteInfo($object::class, $property, []); + if (PropertyWriteInfo::TYPE_PROPERTY === $mutatorForArray->getType()) { + return 'public' === $mutatorForArray->getVisibility(); + } + + if (PropertyWriteInfo::TYPE_NONE !== $mutatorForArray->getType()) { + return true; + } + + $mutator = $this->getWriteInfo($object::class, $property, ''); + + return PropertyWriteInfo::TYPE_NONE !== $mutator->getType(); + } + + /** + * Gets a PropertyPath instance and caches it. + */ + private function getPropertyPath(string|PropertyPath $propertyPath): PropertyPath + { + if ($propertyPath instanceof PropertyPathInterface) { + // Don't call the copy constructor has it is not needed here + return $propertyPath; + } + + if (isset($this->propertyPathCache[$propertyPath])) { + return $this->propertyPathCache[$propertyPath]; + } + + if ($this->cacheItemPool) { + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.rawurlencode($propertyPath)); + if ($item->isHit()) { + return $this->propertyPathCache[$propertyPath] = $item->get(); + } + } + + $propertyPathInstance = new PropertyPath($propertyPath); + if (isset($item)) { + $item->set($propertyPathInstance); + $this->cacheItemPool->save($item); + } + + return $this->propertyPathCache[$propertyPath] = $propertyPathInstance; + } + + /** + * Creates the APCu adapter if applicable. + * + * @throws \LogicException When the Cache Component isn't available + */ + public static function createCache(string $namespace, int $defaultLifetime, string $version, ?LoggerInterface $logger = null): AdapterInterface + { + if (!class_exists(ApcuAdapter::class)) { + throw new \LogicException(\sprintf('The Symfony Cache component must be installed to use "%s()".', __METHOD__)); + } + + if (!ApcuAdapter::isSupported()) { + return new NullAdapter(); + } + + $apcu = new ApcuAdapter($namespace, $defaultLifetime / 5, $version); + if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) { + $apcu->setLogger(new NullLogger()); + } elseif (null !== $logger) { + $apcu->setLogger($logger); + } + + return $apcu; + } +} diff --git a/lib/symfony/property-access/PropertyAccessorBuilder.php b/lib/symfony/property-access/PropertyAccessorBuilder.php new file mode 100644 index 0000000000..431a7f4bf6 --- /dev/null +++ b/lib/symfony/property-access/PropertyAccessorBuilder.php @@ -0,0 +1,294 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; + +/** + * A configurable builder to create a PropertyAccessor. + * + * @author Jérémie Augustin + */ +class PropertyAccessorBuilder +{ + private int $magicMethods = PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET; + private bool $throwExceptionOnInvalidIndex = false; + private bool $throwExceptionOnInvalidPropertyPath = true; + private ?CacheItemPoolInterface $cacheItemPool = null; + private ?PropertyReadInfoExtractorInterface $readInfoExtractor = null; + private ?PropertyWriteInfoExtractorInterface $writeInfoExtractor = null; + + /** + * Enables the use of all magic methods by the PropertyAccessor. + * + * @return $this + */ + public function enableMagicMethods(): static + { + $this->magicMethods = PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_CALL; + + return $this; + } + + /** + * Disable the use of all magic methods by the PropertyAccessor. + * + * @return $this + */ + public function disableMagicMethods(): static + { + $this->magicMethods = PropertyAccessor::DISALLOW_MAGIC_METHODS; + + return $this; + } + + /** + * Enables the use of "__call" by the PropertyAccessor. + * + * @return $this + */ + public function enableMagicCall(): static + { + $this->magicMethods |= PropertyAccessor::MAGIC_CALL; + + return $this; + } + + /** + * Enables the use of "__get" by the PropertyAccessor. + */ + public function enableMagicGet(): self + { + $this->magicMethods |= PropertyAccessor::MAGIC_GET; + + return $this; + } + + /** + * Enables the use of "__set" by the PropertyAccessor. + * + * @return $this + */ + public function enableMagicSet(): static + { + $this->magicMethods |= PropertyAccessor::MAGIC_SET; + + return $this; + } + + /** + * Disables the use of "__call" by the PropertyAccessor. + * + * @return $this + */ + public function disableMagicCall(): static + { + $this->magicMethods &= ~PropertyAccessor::MAGIC_CALL; + + return $this; + } + + /** + * Disables the use of "__get" by the PropertyAccessor. + * + * @return $this + */ + public function disableMagicGet(): static + { + $this->magicMethods &= ~PropertyAccessor::MAGIC_GET; + + return $this; + } + + /** + * Disables the use of "__set" by the PropertyAccessor. + * + * @return $this + */ + public function disableMagicSet(): static + { + $this->magicMethods &= ~PropertyAccessor::MAGIC_SET; + + return $this; + } + + /** + * @return bool whether the use of "__call" by the PropertyAccessor is enabled + */ + public function isMagicCallEnabled(): bool + { + return $this->magicMethods & PropertyAccessor::MAGIC_CALL; + } + + /** + * @return bool whether the use of "__get" by the PropertyAccessor is enabled + */ + public function isMagicGetEnabled(): bool + { + return $this->magicMethods & PropertyAccessor::MAGIC_GET; + } + + /** + * @return bool whether the use of "__set" by the PropertyAccessor is enabled + */ + public function isMagicSetEnabled(): bool + { + return $this->magicMethods & PropertyAccessor::MAGIC_SET; + } + + /** + * Enables exceptions when reading a non-existing index. + * + * This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue() + * which are always created on-the-fly. + * + * @return $this + */ + public function enableExceptionOnInvalidIndex(): static + { + $this->throwExceptionOnInvalidIndex = true; + + return $this; + } + + /** + * Disables exceptions when reading a non-existing index. + * + * Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index. + * + * @return $this + */ + public function disableExceptionOnInvalidIndex(): static + { + $this->throwExceptionOnInvalidIndex = false; + + return $this; + } + + /** + * @return bool whether an exception is thrown or null is returned when reading a non-existing index + */ + public function isExceptionOnInvalidIndexEnabled(): bool + { + return $this->throwExceptionOnInvalidIndex; + } + + /** + * Enables exceptions when reading a non-existing property. + * + * This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue() + * which are always created on-the-fly. + * + * @return $this + */ + public function enableExceptionOnInvalidPropertyPath(): static + { + $this->throwExceptionOnInvalidPropertyPath = true; + + return $this; + } + + /** + * Disables exceptions when reading a non-existing index. + * + * Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index. + * + * @return $this + */ + public function disableExceptionOnInvalidPropertyPath(): static + { + $this->throwExceptionOnInvalidPropertyPath = false; + + return $this; + } + + /** + * @return bool whether an exception is thrown or null is returned when reading a non-existing property + */ + public function isExceptionOnInvalidPropertyPath(): bool + { + return $this->throwExceptionOnInvalidPropertyPath; + } + + /** + * Sets a cache system. + * + * @return $this + */ + public function setCacheItemPool(?CacheItemPoolInterface $cacheItemPool = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/property-access', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->cacheItemPool = $cacheItemPool; + + return $this; + } + + /** + * Gets the used cache system. + */ + public function getCacheItemPool(): ?CacheItemPoolInterface + { + return $this->cacheItemPool; + } + + /** + * @return $this + */ + public function setReadInfoExtractor(?PropertyReadInfoExtractorInterface $readInfoExtractor): static + { + $this->readInfoExtractor = $readInfoExtractor; + + return $this; + } + + public function getReadInfoExtractor(): ?PropertyReadInfoExtractorInterface + { + return $this->readInfoExtractor; + } + + /** + * @return $this + */ + public function setWriteInfoExtractor(?PropertyWriteInfoExtractorInterface $writeInfoExtractor): static + { + $this->writeInfoExtractor = $writeInfoExtractor; + + return $this; + } + + public function getWriteInfoExtractor(): ?PropertyWriteInfoExtractorInterface + { + return $this->writeInfoExtractor; + } + + /** + * Builds and returns a new PropertyAccessor object. + */ + public function getPropertyAccessor(): PropertyAccessorInterface + { + $throw = PropertyAccessor::DO_NOT_THROW; + + if ($this->throwExceptionOnInvalidIndex) { + $throw |= PropertyAccessor::THROW_ON_INVALID_INDEX; + } + + if ($this->throwExceptionOnInvalidPropertyPath) { + $throw |= PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH; + } + + return new PropertyAccessor($this->magicMethods, $throw, $this->cacheItemPool, $this->readInfoExtractor, $this->writeInfoExtractor); + } +} diff --git a/lib/symfony/property-access/PropertyAccessorInterface.php b/lib/symfony/property-access/PropertyAccessorInterface.php new file mode 100644 index 0000000000..c1947beeaf --- /dev/null +++ b/lib/symfony/property-access/PropertyAccessorInterface.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Writes and reads values to/from an object/array graph. + * + * @author Bernhard Schussek + */ +interface PropertyAccessorInterface +{ + /** + * Sets the value at the end of the property path of the object graph. + * + * Example: + * + * use Symfony\Component\PropertyAccess\PropertyAccess; + * + * $propertyAccessor = PropertyAccess::createPropertyAccessor(); + * + * echo $propertyAccessor->setValue($object, 'child.name', 'Fabien'); + * // equals echo $object->getChild()->setName('Fabien'); + * + * This method first tries to find a public setter for each property in the + * path. The name of the setter must be the camel-cased property name + * prefixed with "set". + * + * If the setter does not exist, this method tries to find a public + * property. The value of the property is then changed. + * + * If neither is found, an exception is thrown. + * + * @return void + * + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\AccessException If a property/index does not exist or is not public + * @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array + */ + public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value); + + /** + * Returns the value at the end of the property path of the object graph. + * + * Example: + * + * use Symfony\Component\PropertyAccess\PropertyAccess; + * + * $propertyAccessor = PropertyAccess::createPropertyAccessor(); + * + * echo $propertyAccessor->getValue($object, 'child.name'); + * // equals echo $object->getChild()->getName(); + * + * This method first tries to find a public getter for each property in the + * path. The name of the getter must be the camel-cased property name + * prefixed with "get", "is", or "has". + * + * If the getter does not exist, this method tries to find a public + * property. The value of the property is then returned. + * + * If none of them are found, an exception is thrown. + * + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\AccessException If a property/index does not exist or is not public + * @throws Exception\UnexpectedTypeException If a value within the path is neither object + * nor array + */ + public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed; + + /** + * Returns whether a value can be written at a given property path. + * + * Whenever this method returns true, {@link setValue()} is guaranteed not + * to throw an exception when called with the same arguments. + * + * @throws Exception\InvalidArgumentException If the property path is invalid + */ + public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool; + + /** + * Returns whether a property path can be read from an object graph. + * + * Whenever this method returns true, {@link getValue()} is guaranteed not + * to throw an exception when called with the same arguments. + * + * @throws Exception\InvalidArgumentException If the property path is invalid + */ + public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool; +} diff --git a/lib/symfony/property-access/PropertyPath.php b/lib/symfony/property-access/PropertyPath.php new file mode 100644 index 0000000000..5cb4376616 --- /dev/null +++ b/lib/symfony/property-access/PropertyPath.php @@ -0,0 +1,205 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; +use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; + +/** + * Default implementation of {@link PropertyPathInterface}. + * + * @author Bernhard Schussek + * + * @implements \IteratorAggregate + */ +class PropertyPath implements \IteratorAggregate, PropertyPathInterface +{ + /** + * Character used for separating between plural and singular of an element. + */ + public const SINGULAR_SEPARATOR = '|'; + + /** + * The elements of the property path. + * + * @var list + */ + private array $elements = []; + + /** + * The number of elements in the property path. + */ + private int $length; + + /** + * Contains a Boolean for each property in $elements denoting whether this + * element is an index. It is a property otherwise. + * + * @var array + */ + private array $isIndex = []; + + /** + * Contains a Boolean for each property in $elements denoting whether this + * element is optional or not. + * + * @var array + */ + private array $isNullSafe = []; + + /** + * String representation of the path. + */ + private string $pathAsString; + + /** + * Constructs a property path from a string. + * + * @throws InvalidArgumentException If the given path is not a string + * @throws InvalidPropertyPathException If the syntax of the property path is not valid + */ + public function __construct(self|string $propertyPath) + { + // Can be used as copy constructor + if ($propertyPath instanceof self) { + $this->elements = $propertyPath->elements; + $this->length = $propertyPath->length; + $this->isIndex = $propertyPath->isIndex; + $this->isNullSafe = $propertyPath->isNullSafe; + $this->pathAsString = $propertyPath->pathAsString; + + return; + } + + if ('' === $propertyPath) { + throw new InvalidPropertyPathException('The property path should not be empty.'); + } + + $this->pathAsString = $propertyPath; + $position = 0; + $remaining = $propertyPath; + + // first element is evaluated differently - no leading dot for properties + $pattern = '/^(((?:[^\\\\.\[]|\\\\.)++)|\[([^\]]++)\])(.*)/'; + + while (preg_match($pattern, $remaining, $matches)) { + if ('' !== $matches[2]) { + $element = $matches[2]; + $this->isIndex[] = false; + } else { + $element = $matches[3]; + $this->isIndex[] = true; + } + + // Mark as optional when last character is "?". + if (str_ends_with($element, '?')) { + $this->isNullSafe[] = true; + $element = substr($element, 0, -1); + } else { + $this->isNullSafe[] = false; + } + + $element = preg_replace('/\\\([.[])/', '$1', $element); + if (str_ends_with($element, '\\\\')) { + $element = substr($element, 0, -1); + } + $this->elements[] = $element; + + $position += \strlen($matches[1]); + $remaining = $matches[4]; + $pattern = '/^(\.((?:[^\\\\.\[]|\\\\.)++)|\[([^\]]++)\])(.*)/'; + } + + if ('' !== $remaining) { + throw new InvalidPropertyPathException(\sprintf('Could not parse property path "%s". Unexpected token "%s" at position %d.', $propertyPath, $remaining[0], $position)); + } + + $this->length = \count($this->elements); + } + + public function __toString(): string + { + return $this->pathAsString; + } + + public function getLength(): int + { + return $this->length; + } + + public function getParent(): ?PropertyPathInterface + { + if ($this->length <= 1) { + return null; + } + + $parent = clone $this; + + --$parent->length; + $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '['))); + array_pop($parent->elements); + array_pop($parent->isIndex); + array_pop($parent->isNullSafe); + + return $parent; + } + + /** + * Returns a new iterator for this path. + */ + public function getIterator(): PropertyPathIteratorInterface + { + return new PropertyPathIterator($this); + } + + public function getElements(): array + { + return $this->elements; + } + + public function getElement(int $index): string + { + if (!isset($this->elements[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); + } + + return $this->elements[$index]; + } + + public function isProperty(int $index): bool + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); + } + + return !$this->isIndex[$index]; + } + + public function isIndex(int $index): bool + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); + } + + return $this->isIndex[$index]; + } + + public function isNullSafe(int $index): bool + { + if (!isset($this->isNullSafe[$index])) { + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); + } + + return $this->isNullSafe[$index]; + } +} diff --git a/lib/symfony/property-access/PropertyPathBuilder.php b/lib/symfony/property-access/PropertyPathBuilder.php new file mode 100644 index 0000000000..8dd610675d --- /dev/null +++ b/lib/symfony/property-access/PropertyPathBuilder.php @@ -0,0 +1,275 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; + +/** + * @author Bernhard Schussek + */ +class PropertyPathBuilder +{ + private array $elements = []; + private array $isIndex = []; + + public function __construct(PropertyPathInterface|string|null $path = null) + { + if (null !== $path) { + $this->append($path); + } + } + + /** + * Appends a (sub-) path to the current path. + * + * @param int $offset The offset where the appended piece starts in $path + * @param int $length The length of the appended piece; if 0, the full path is appended + * + * @return void + */ + public function append(PropertyPathInterface|string $path, int $offset = 0, int $length = 0) + { + if (\is_string($path)) { + $path = new PropertyPath($path); + } + + if (0 === $length) { + $end = $path->getLength(); + } else { + $end = $offset + $length; + } + + for (; $offset < $end; ++$offset) { + $this->elements[] = $path->getElement($offset); + $this->isIndex[] = $path->isIndex($offset); + } + } + + /** + * Appends an index element to the current path. + * + * @return void + */ + public function appendIndex(string $name) + { + $this->elements[] = $name; + $this->isIndex[] = true; + } + + /** + * Appends a property element to the current path. + * + * @return void + */ + public function appendProperty(string $name) + { + $this->elements[] = $name; + $this->isIndex[] = false; + } + + /** + * Removes elements from the current path. + * + * @return void + * + * @throws OutOfBoundsException if offset is invalid + */ + public function remove(int $offset, int $length = 1) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException(\sprintf('The offset "%s" is not within the property path.', $offset)); + } + + $this->resize($offset, $length, 0); + } + + /** + * Replaces a sub-path by a different (sub-) path. + * + * @param int $pathOffset The offset where the inserted piece starts in $path + * @param int $pathLength The length of the inserted piece; if 0, the full path is inserted + * + * @return void + * + * @throws OutOfBoundsException If the offset is invalid + */ + public function replace(int $offset, int $length, PropertyPathInterface|string $path, int $pathOffset = 0, int $pathLength = 0) + { + if (\is_string($path)) { + $path = new PropertyPath($path); + } + + if ($offset < 0 && abs($offset) <= $this->getLength()) { + $offset = $this->getLength() + $offset; + } elseif (!isset($this->elements[$offset])) { + throw new OutOfBoundsException('The offset '.$offset.' is not within the property path'); + } + + if (0 === $pathLength) { + $pathLength = $path->getLength() - $pathOffset; + } + + $this->resize($offset, $length, $pathLength); + + for ($i = 0; $i < $pathLength; ++$i) { + $this->elements[$offset + $i] = $path->getElement($pathOffset + $i); + $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i); + } + ksort($this->elements); + } + + /** + * Replaces a property element by an index element. + * + * @return void + * + * @throws OutOfBoundsException If the offset is invalid + */ + public function replaceByIndex(int $offset, ?string $name = null) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException(\sprintf('The offset "%s" is not within the property path.', $offset)); + } + + if (null !== $name) { + $this->elements[$offset] = $name; + } + + $this->isIndex[$offset] = true; + } + + /** + * Replaces an index element by a property element. + * + * @return void + * + * @throws OutOfBoundsException If the offset is invalid + */ + public function replaceByProperty(int $offset, ?string $name = null) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException(\sprintf('The offset "%s" is not within the property path.', $offset)); + } + + if (null !== $name) { + $this->elements[$offset] = $name; + } + + $this->isIndex[$offset] = false; + } + + /** + * Returns the length of the current path. + */ + public function getLength(): int + { + return \count($this->elements); + } + + /** + * Returns the current property path. + */ + public function getPropertyPath(): ?PropertyPathInterface + { + $pathAsString = $this->__toString(); + + return '' !== $pathAsString ? new PropertyPath($pathAsString) : null; + } + + /** + * Returns the current property path as string. + */ + public function __toString(): string + { + $string = ''; + + foreach ($this->elements as $offset => $element) { + if ($this->isIndex[$offset]) { + $element = '['.$element.']'; + } elseif ('' !== $string) { + $string .= '.'; + } + + $string .= $element; + } + + return $string; + } + + /** + * Resizes the path so that a chunk of length $cutLength is + * removed at $offset and another chunk of length $insertionLength + * can be inserted. + */ + private function resize(int $offset, int $cutLength, int $insertionLength): void + { + // Nothing else to do in this case + if ($insertionLength === $cutLength) { + return; + } + + $length = \count($this->elements); + + if ($cutLength > $insertionLength) { + // More elements should be removed than inserted + $diff = $cutLength - $insertionLength; + $newLength = $length - $diff; + + // Shift elements to the left (left-to-right until the new end) + // Max allowed offset to be shifted is such that + // $offset + $diff < $length (otherwise invalid index access) + // i.e. $offset < $length - $diff = $newLength + for ($i = $offset; $i < $newLength; ++$i) { + $this->elements[$i] = $this->elements[$i + $diff]; + $this->isIndex[$i] = $this->isIndex[$i + $diff]; + } + + // All remaining elements should be removed + $this->elements = \array_slice($this->elements, 0, $i); + $this->isIndex = \array_slice($this->isIndex, 0, $i); + } else { + $diff = $insertionLength - $cutLength; + + $newLength = $length + $diff; + $indexAfterInsertion = $offset + $insertionLength; + + // $diff <= $insertionLength + // $indexAfterInsertion >= $insertionLength + // => $diff <= $indexAfterInsertion + + // In each of the following loops, $i >= $diff must hold, + // otherwise ($i - $diff) becomes negative. + + // Shift old elements to the right to make up space for the + // inserted elements. This needs to be done left-to-right in + // order to preserve an ascending array index order + // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff, + // $i >= $diff is guaranteed. + for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) { + $this->elements[$i] = $this->elements[$i - $diff]; + $this->isIndex[$i] = $this->isIndex[$i - $diff]; + } + + // Shift remaining elements to the right. Do this right-to-left + // so we don't overwrite elements before copying them + // The last written index is the immediate index after the inserted + // string, because the indices before that will be overwritten + // anyway. + // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff, + // $i >= $diff is guaranteed. + for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) { + $this->elements[$i] = $this->elements[$i - $diff]; + $this->isIndex[$i] = $this->isIndex[$i - $diff]; + } + } + } +} diff --git a/lib/symfony/property-access/PropertyPathInterface.php b/lib/symfony/property-access/PropertyPathInterface.php new file mode 100644 index 0000000000..324cbe5c48 --- /dev/null +++ b/lib/symfony/property-access/PropertyPathInterface.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * A sequence of property names or array indices. + * + * @author Bernhard Schussek + * + * @method bool isNullSafe(int $index) Returns whether the element at the given index is null safe. Not implementing it is deprecated since Symfony 6.2 + * + * @extends \Traversable + */ +interface PropertyPathInterface extends \Traversable, \Stringable +{ + /** + * Returns the string representation of the property path. + */ + public function __toString(): string; + + /** + * Returns the length of the property path, i.e. the number of elements. + * + * @return int + */ + public function getLength(); + + /** + * Returns the parent property path. + * + * The parent property path is the one that contains the same items as + * this one except for the last one. + * + * If this property path only contains one item, null is returned. + * + * @return self|null + */ + public function getParent(); + + /** + * Returns the elements of the property path as array. + * + * @return list + */ + public function getElements(); + + /** + * Returns the element at the given index in the property path. + * + * @param int $index The index key + * + * @return string + * + * @throws Exception\OutOfBoundsException If the offset is invalid + */ + public function getElement(int $index); + + /** + * Returns whether the element at the given index is a property. + * + * @param int $index The index in the property path + * + * @return bool + * + * @throws Exception\OutOfBoundsException If the offset is invalid + */ + public function isProperty(int $index); + + /** + * Returns whether the element at the given index is an array index. + * + * @param int $index The index in the property path + * + * @return bool + * + * @throws Exception\OutOfBoundsException If the offset is invalid + */ + public function isIndex(int $index); +} diff --git a/lib/symfony/property-access/PropertyPathIterator.php b/lib/symfony/property-access/PropertyPathIterator.php new file mode 100644 index 0000000000..0312ba156c --- /dev/null +++ b/lib/symfony/property-access/PropertyPathIterator.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Traverses a property path and provides additional methods to find out + * information about the current element. + * + * @author Bernhard Schussek + * + * @extends \ArrayIterator + */ +class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface +{ + protected $path; + + public function __construct(PropertyPathInterface $path) + { + parent::__construct($path->getElements()); + + $this->path = $path; + } + + public function isIndex(): bool + { + return $this->path->isIndex($this->key()); + } + + public function isProperty(): bool + { + return $this->path->isProperty($this->key()); + } +} diff --git a/lib/symfony/property-access/PropertyPathIteratorInterface.php b/lib/symfony/property-access/PropertyPathIteratorInterface.php new file mode 100644 index 0000000000..4704b36ab5 --- /dev/null +++ b/lib/symfony/property-access/PropertyPathIteratorInterface.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\PropertyAccess; + +/** + * @author Bernhard Schussek + * + * @extends \SeekableIterator + */ +interface PropertyPathIteratorInterface extends \SeekableIterator +{ + /** + * Returns whether the current element in the property path is an array + * index. + */ + public function isIndex(): bool; + + /** + * Returns whether the current element in the property path is a property + * name. + */ + public function isProperty(): bool; +} diff --git a/lib/symfony/property-access/README.md b/lib/symfony/property-access/README.md new file mode 100644 index 0000000000..29cb233a01 --- /dev/null +++ b/lib/symfony/property-access/README.md @@ -0,0 +1,14 @@ +PropertyAccess Component +======================== + +The PropertyAccess component provides functions to read and write from/to an +object or array using a simple string notation. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/property_access.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/lib/symfony/property-access/composer.json b/lib/symfony/property-access/composer.json new file mode 100644 index 0000000000..ce7710cfe1 --- /dev/null +++ b/lib/symfony/property-access/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/property-access", + "type": "library", + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property-path"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/property-info": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\PropertyAccess\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/lib/symfony/property-info/CHANGELOG.md b/lib/symfony/property-info/CHANGELOG.md new file mode 100644 index 0000000000..ce7f220ce1 --- /dev/null +++ b/lib/symfony/property-info/CHANGELOG.md @@ -0,0 +1,57 @@ +CHANGELOG +========= + +6.4 +--- + + * Make properties writable when a setter in camelCase exists, similar to the camelCase getter + +6.1 +--- + + * Add support for phpDocumentor and PHPStan pseudo-types + * Add PHP 8.0 promoted properties `@param` mutation support to `PhpDocExtractor` + * Add PHP 8.0 promoted properties `@param` mutation support to `PhpStanExtractor` + +6.0 +--- + + * Remove the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead + * Remove the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction` + +5.4 +--- + + * Add PhpStanExtractor + +5.3 +--- + + * Add support for multiple types for collection keys & values + * Deprecate the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead + +5.2.0 +----- + + * deprecated the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction` + +5.1.0 +----- + + * Add support for extracting accessor and mutator via PHP Reflection + +4.3.0 +----- + + * Added the ability to extract private and protected properties and methods on `ReflectionExtractor` + * Added the ability to extract property type based on its initial value + +4.2.0 +----- + + * added `PropertyInitializableExtractorInterface` to test if a property can be initialized through the constructor (implemented by `ReflectionExtractor`) + +3.3.0 +----- + + * Added `PropertyInfoPass` diff --git a/lib/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php b/lib/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php new file mode 100644 index 0000000000..6c775384d9 --- /dev/null +++ b/lib/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Adds extractors to the property_info.constructor_extractor service. + * + * @author Dmitrii Poddubnyi + */ +final class PropertyInfoConstructorPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('property_info.constructor_extractor')) { + return; + } + $definition = $container->getDefinition('property_info.constructor_extractor'); + + $listExtractors = $this->findAndSortTaggedServices('property_info.constructor_extractor', $container); + $definition->replaceArgument(0, new IteratorArgument($listExtractors)); + } +} diff --git a/lib/symfony/property-info/DependencyInjection/PropertyInfoPass.php b/lib/symfony/property-info/DependencyInjection/PropertyInfoPass.php new file mode 100644 index 0000000000..1c240b43da --- /dev/null +++ b/lib/symfony/property-info/DependencyInjection/PropertyInfoPass.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\PropertyInfo\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Adds extractors to the property_info service. + * + * @author Kévin Dunglas + */ +class PropertyInfoPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('property_info')) { + return; + } + + $definition = $container->getDefinition('property_info'); + + $listExtractors = $this->findAndSortTaggedServices('property_info.list_extractor', $container); + $definition->replaceArgument(0, new IteratorArgument($listExtractors)); + + $typeExtractors = $this->findAndSortTaggedServices('property_info.type_extractor', $container); + $definition->replaceArgument(1, new IteratorArgument($typeExtractors)); + + $descriptionExtractors = $this->findAndSortTaggedServices('property_info.description_extractor', $container); + $definition->replaceArgument(2, new IteratorArgument($descriptionExtractors)); + + $accessExtractors = $this->findAndSortTaggedServices('property_info.access_extractor', $container); + $definition->replaceArgument(3, new IteratorArgument($accessExtractors)); + + $initializableExtractors = $this->findAndSortTaggedServices('property_info.initializable_extractor', $container); + $definition->setArgument(4, new IteratorArgument($initializableExtractors)); + } +} diff --git a/lib/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php b/lib/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php new file mode 100644 index 0000000000..cbde902e98 --- /dev/null +++ b/lib/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\Type; + +/** + * Infers the constructor argument type. + * + * @author Dmitrii Poddubnyi + * + * @internal + */ +interface ConstructorArgumentTypeExtractorInterface +{ + /** + * Gets types of an argument from constructor. + * + * @return Type[]|null + * + * @internal + */ + public function getTypesFromConstructor(string $class, string $property): ?array; +} diff --git a/lib/symfony/property-info/Extractor/ConstructorExtractor.php b/lib/symfony/property-info/Extractor/ConstructorExtractor.php new file mode 100644 index 0000000000..18e563a718 --- /dev/null +++ b/lib/symfony/property-info/Extractor/ConstructorExtractor.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; + +/** + * Extracts the constructor argument type using ConstructorArgumentTypeExtractorInterface implementations. + * + * @author Dmitrii Poddubnyi + */ +final class ConstructorExtractor implements PropertyTypeExtractorInterface +{ + /** + * @param iterable $extractors + */ + public function __construct( + private readonly iterable $extractors = [], + ) { + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + foreach ($this->extractors as $extractor) { + $value = $extractor->getTypesFromConstructor($class, $property); + if (null !== $value) { + return $value; + } + } + + return null; + } +} diff --git a/lib/symfony/property-info/Extractor/PhpDocExtractor.php b/lib/symfony/property-info/Extractor/PhpDocExtractor.php new file mode 100644 index 0000000000..e1b42eec5c --- /dev/null +++ b/lib/symfony/property-info/Extractor/PhpDocExtractor.php @@ -0,0 +1,348 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; +use phpDocumentor\Reflection\DocBlockFactory; +use phpDocumentor\Reflection\DocBlockFactoryInterface; +use phpDocumentor\Reflection\Types\Context; +use phpDocumentor\Reflection\Types\ContextFactory; +use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper; + +/** + * Extracts data using a PHPDoc parser. + * + * @author Kévin Dunglas + * + * @final + */ +class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface +{ + public const PROPERTY = 0; + public const ACCESSOR = 1; + public const MUTATOR = 2; + + /** + * @var array + */ + private array $docBlocks = []; + + /** + * @var Context[] + */ + private array $contexts = []; + + private DocBlockFactoryInterface $docBlockFactory; + private ContextFactory $contextFactory; + private PhpDocTypeHelper $phpDocTypeHelper; + private array $mutatorPrefixes; + private array $accessorPrefixes; + private array $arrayMutatorPrefixes; + + /** + * @param string[]|null $mutatorPrefixes + * @param string[]|null $accessorPrefixes + * @param string[]|null $arrayMutatorPrefixes + */ + public function __construct(?DocBlockFactoryInterface $docBlockFactory = null, ?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null) + { + if (!class_exists(DocBlockFactory::class)) { + throw new \LogicException(\sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed. Try running composer require "phpdocumentor/reflection-docblock".', __CLASS__)); + } + + $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); + $this->contextFactory = new ContextFactory(); + $this->phpDocTypeHelper = new PhpDocTypeHelper(); + $this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes; + $this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes; + $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes; + } + + public function getShortDescription(string $class, string $property, array $context = []): ?string + { + /** @var DocBlock $docBlock */ + [$docBlock] = $this->getDocBlock($class, $property); + if (!$docBlock) { + return null; + } + + $shortDescription = $docBlock->getSummary(); + + if (!empty($shortDescription)) { + return $shortDescription; + } + + foreach ($docBlock->getTagsByName('var') as $var) { + if ($var && !$var instanceof InvalidTag) { + $varDescription = $var->getDescription()->render(); + + if (!empty($varDescription)) { + return $varDescription; + } + } + } + + return null; + } + + public function getLongDescription(string $class, string $property, array $context = []): ?string + { + /** @var DocBlock $docBlock */ + [$docBlock] = $this->getDocBlock($class, $property); + if (!$docBlock) { + return null; + } + + $contents = $docBlock->getDescription()->render(); + + return '' === $contents ? null : $contents; + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + /** @var DocBlock $docBlock */ + [$docBlock, $source, $prefix] = $this->getDocBlock($class, $property); + if (!$docBlock) { + return null; + } + + $tag = match ($source) { + self::PROPERTY => 'var', + self::ACCESSOR => 'return', + self::MUTATOR => 'param', + }; + + $parentClass = null; + $types = []; + /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */ + foreach ($docBlock->getTagsByName($tag) as $tag) { + if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) { + foreach ($this->phpDocTypeHelper->getTypes($tag->getType()) as $type) { + switch ($type->getClassName()) { + case 'self': + case 'static': + $resolvedClass = $class; + break; + + case 'parent': + if (false !== $resolvedClass = $parentClass ??= get_parent_class($class)) { + break; + } + // no break + + default: + $types[] = $type; + continue 2; + } + + $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); + } + } + } + + if (!isset($types[0])) { + return null; + } + + if (!\in_array($prefix, $this->arrayMutatorPrefixes)) { + return $types; + } + + return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])]; + } + + public function getTypesFromConstructor(string $class, string $property): ?array + { + $docBlock = $this->getDocBlockFromConstructor($class, $property); + + if (!$docBlock) { + return null; + } + + $types = []; + /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */ + foreach ($docBlock->getTagsByName('param') as $tag) { + if ($tag && null !== $tag->getType()) { + $types[] = $this->phpDocTypeHelper->getTypes($tag->getType()); + } + } + + if (!isset($types[0]) || [] === $types[0]) { + return null; + } + + return array_merge([], ...$types); + } + + private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + $reflectionConstructor = $reflectionClass->getConstructor(); + if (!$reflectionConstructor) { + return null; + } + + try { + $docBlock = $this->docBlockFactory->create($reflectionConstructor, $this->contextFactory->createFromReflector($reflectionConstructor)); + + return $this->filterDocBlockParams($docBlock, $property); + } catch (\InvalidArgumentException) { + return null; + } + } + + private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): DocBlock + { + $tags = array_values(array_filter($docBlock->getTagsByName('param'), fn ($tag) => $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName())); + + return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags, $docBlock->getContext(), + $docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd()); + } + + /** + * @return array{DocBlock|null, int|null, string|null} + */ + private function getDocBlock(string $class, string $property): array + { + $propertyHash = \sprintf('%s::%s', $class, $property); + + if (isset($this->docBlocks[$propertyHash])) { + return $this->docBlocks[$propertyHash]; + } + + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + } catch (\ReflectionException) { + $reflectionProperty = null; + } + + $ucFirstProperty = ucfirst($property); + + switch (true) { + case $reflectionProperty?->isPromoted() && $docBlock = $this->getDocBlockFromConstructor($class, $property): + $data = [$docBlock, self::MUTATOR, null]; + break; + + case $docBlock = $this->getDocBlockFromProperty($class, $property): + $data = [$docBlock, self::PROPERTY, null]; + break; + + case [$docBlock] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR): + $data = [$docBlock, self::ACCESSOR, null]; + break; + + case [$docBlock, $prefix] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR): + $data = [$docBlock, self::MUTATOR, $prefix]; + break; + + default: + $data = [null, null, null]; + } + + return $this->docBlocks[$propertyHash] = $data; + } + + private function getDocBlockFromProperty(string $class, string $property): ?DocBlock + { + // Use a ReflectionProperty instead of $class to get the parent class if applicable + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + } catch (\ReflectionException) { + return null; + } + + $reflector = $reflectionProperty->getDeclaringClass(); + + foreach ($reflector->getTraits() as $trait) { + if ($trait->hasProperty($property)) { + return $this->getDocBlockFromProperty($trait->getName(), $property); + } + } + + try { + return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector)); + } catch (\InvalidArgumentException|\RuntimeException) { + return null; + } + } + + /** + * @return array{DocBlock, string}|null + */ + private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array + { + $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes; + $prefix = null; + + foreach ($prefixes as $prefix) { + $methodName = $prefix.$ucFirstProperty; + + try { + $reflectionMethod = new \ReflectionMethod($class, $methodName); + if ($reflectionMethod->isStatic()) { + continue; + } + + if ( + (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) + || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1) + ) { + break; + } + } catch (\ReflectionException) { + // Try the next prefix if the method doesn't exist + } + } + + if (!isset($reflectionMethod)) { + return null; + } + + $reflector = $reflectionMethod->getDeclaringClass(); + + foreach ($reflector->getTraits() as $trait) { + if ($trait->hasMethod($methodName)) { + return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty, $type); + } + } + + try { + return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix]; + } catch (\InvalidArgumentException|\RuntimeException) { + return null; + } + } + + /** + * Prevents a lot of redundant calls to ContextFactory::createForNamespace(). + */ + private function createFromReflector(\ReflectionClass $reflector): Context + { + $cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName(); + + if (isset($this->contexts[$cacheKey])) { + return $this->contexts[$cacheKey]; + } + + $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector); + + return $this->contexts[$cacheKey]; + } +} diff --git a/lib/symfony/property-info/Extractor/PhpStanExtractor.php b/lib/symfony/property-info/Extractor/PhpStanExtractor.php new file mode 100644 index 0000000000..d79a6a10a6 --- /dev/null +++ b/lib/symfony/property-info/Extractor/PhpStanExtractor.php @@ -0,0 +1,323 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use phpDocumentor\Reflection\Types\ContextFactory; +use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Lexer\Lexer; +use PHPStan\PhpDocParser\Parser\ConstExprParser; +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\PhpDocParser\Parser\TokenIterator; +use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; +use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper; + +/** + * Extracts data using PHPStan parser. + * + * @author Baptiste Leduc + */ +final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface +{ + private const PROPERTY = 0; + private const ACCESSOR = 1; + private const MUTATOR = 2; + + private PhpDocParser $phpDocParser; + private Lexer $lexer; + private NameScopeFactory $nameScopeFactory; + + /** @var array */ + private array $docBlocks = []; + private PhpStanTypeHelper $phpStanTypeHelper; + private array $mutatorPrefixes; + private array $accessorPrefixes; + private array $arrayMutatorPrefixes; + + /** + * @param list|null $mutatorPrefixes + * @param list|null $accessorPrefixes + * @param list|null $arrayMutatorPrefixes + */ + public function __construct(?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null) + { + if (!class_exists(ContextFactory::class)) { + throw new \LogicException(\sprintf('Unable to use the "%s" class as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".', __CLASS__)); + } + + if (!class_exists(PhpDocParser::class)) { + throw new \LogicException(\sprintf('Unable to use the "%s" class as the "phpstan/phpdoc-parser" package is not installed. Try running composer require "phpstan/phpdoc-parser".', __CLASS__)); + } + + $this->phpStanTypeHelper = new PhpStanTypeHelper(); + $this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes; + $this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes; + $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes; + + if (class_exists(ParserConfig::class)) { + $parserConfig = new ParserConfig([]); + $this->phpDocParser = new PhpDocParser($parserConfig, new TypeParser($parserConfig, new ConstExprParser($parserConfig)), new ConstExprParser($parserConfig)); + $this->lexer = new Lexer($parserConfig); + } else { + $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); + $this->lexer = new Lexer(); + } + $this->nameScopeFactory = new NameScopeFactory(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + /** @var PhpDocNode|null $docNode */ + [$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property); + $nameScope = $this->nameScopeFactory->create($class, $declaringClass); + if (null === $docNode) { + return null; + } + + switch ($source) { + case self::PROPERTY: + $tag = '@var'; + break; + + case self::ACCESSOR: + $tag = '@return'; + break; + + case self::MUTATOR: + $tag = '@param'; + break; + } + + $parentClass = null; + $types = []; + foreach ($docNode->getTagsByName($tag) as $tagDocNode) { + if ($tagDocNode->value instanceof InvalidTagValueNode) { + continue; + } + + if ( + $tagDocNode->value instanceof ParamTagValueNode + && null === $prefix + && $tagDocNode->value->parameterName !== '$'.$property + ) { + continue; + } + + foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $nameScope) as $type) { + switch ($type->getClassName()) { + case 'self': + case 'static': + $resolvedClass = $class; + break; + + case 'parent': + if (false !== $resolvedClass = $parentClass ??= get_parent_class($class)) { + break; + } + // no break + + default: + $types[] = $type; + continue 2; + } + + $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); + } + } + + if (!isset($types[0])) { + return null; + } + + if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) { + return $types; + } + + return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])]; + } + + public function getTypesFromConstructor(string $class, string $property): ?array + { + if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) { + return null; + } + + $types = []; + foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->nameScopeFactory->create($class)) as $type) { + $types[] = $type; + } + + if (!isset($types[0])) { + return null; + } + + return $types; + } + + private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + if (null === $reflectionConstructor = $reflectionClass->getConstructor()) { + return null; + } + + if (!$rawDocNode = $reflectionConstructor->getDocComment()) { + return null; + } + + $phpDocNode = $this->getPhpDocNode($rawDocNode); + + return $this->filterDocBlockParams($phpDocNode, $property); + } + + private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam): ?ParamTagValueNode + { + $tags = array_values(array_filter($docNode->getTagsByName('@param'), fn ($tagNode) => $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName)); + + if (!$tags) { + return null; + } + + return $tags[0]->value; + } + + /** + * @return array{PhpDocNode|null, int|null, string|null, string|null} + */ + private function getDocBlock(string $class, string $property): array + { + $propertyHash = $class.'::'.$property; + + if (isset($this->docBlocks[$propertyHash])) { + return $this->docBlocks[$propertyHash]; + } + + $ucFirstProperty = ucfirst($property); + + if ([$docBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) { + $data = [$docBlock, $source, null, $declaringClass]; + } elseif ([$docBlock, $_, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) { + $data = [$docBlock, self::ACCESSOR, null, $declaringClass]; + } elseif ([$docBlock, $prefix, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR)) { + $data = [$docBlock, self::MUTATOR, $prefix, $declaringClass]; + } else { + $data = [null, null, null, null]; + } + + return $this->docBlocks[$propertyHash] = $data; + } + + /** + * @return array{PhpDocNode, int, string}|null + */ + private function getDocBlockFromProperty(string $class, string $property): ?array + { + // Use a ReflectionProperty instead of $class to get the parent class if applicable + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + } catch (\ReflectionException) { + return null; + } + + $reflector = $reflectionProperty->getDeclaringClass(); + + foreach ($reflector->getTraits() as $trait) { + if ($trait->hasProperty($property)) { + return $this->getDocBlockFromProperty($trait->getName(), $property); + } + } + + // Type can be inside property docblock as `@var` + $rawDocNode = $reflectionProperty->getDocComment(); + $phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null; + $source = self::PROPERTY; + + if (!$phpDocNode?->getTagsByName('@var')) { + $phpDocNode = null; + } + + // or in the constructor as `@param` for promoted properties + if (!$phpDocNode && $reflectionProperty->isPromoted()) { + $constructor = new \ReflectionMethod($class, '__construct'); + $rawDocNode = $constructor->getDocComment(); + $phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null; + $source = self::MUTATOR; + } + + if (!$phpDocNode) { + return null; + } + + return [$phpDocNode, $source, $reflectionProperty->class]; + } + + /** + * @return array{PhpDocNode, string, string}|null + */ + private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array + { + $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes; + $prefix = null; + + foreach ($prefixes as $prefix) { + $methodName = $prefix.$ucFirstProperty; + + try { + $reflectionMethod = new \ReflectionMethod($class, $methodName); + if ($reflectionMethod->isStatic()) { + continue; + } + + if ( + (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) + || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1) + ) { + break; + } + } catch (\ReflectionException) { + // Try the next prefix if the method doesn't exist + } + } + + if (!isset($reflectionMethod)) { + return null; + } + + if (null === $rawDocNode = $reflectionMethod->getDocComment() ?: null) { + return null; + } + + $phpDocNode = $this->getPhpDocNode($rawDocNode); + + return [$phpDocNode, $prefix, $reflectionMethod->class]; + } + + private function getPhpDocNode(string $rawDocNode): PhpDocNode + { + $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $tokens->consumeTokenType(Lexer::TOKEN_END); + + return $phpDocNode; + } +} diff --git a/lib/symfony/property-info/Extractor/ReflectionExtractor.php b/lib/symfony/property-info/Extractor/ReflectionExtractor.php new file mode 100644 index 0000000000..6311c55fd3 --- /dev/null +++ b/lib/symfony/property-info/Extractor/ReflectionExtractor.php @@ -0,0 +1,872 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\String\Inflector\EnglishInflector; +use Symfony\Component\String\Inflector\InflectorInterface; + +/** + * Extracts data using the reflection API. + * + * @author Kévin Dunglas + * + * @final + */ +class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface +{ + /** + * @internal + */ + public static array $defaultMutatorPrefixes = ['add', 'remove', 'set']; + + /** + * @internal + */ + public static array $defaultAccessorPrefixes = ['get', 'is', 'has', 'can']; + + /** + * @internal + */ + public static array $defaultArrayMutatorPrefixes = ['add', 'remove']; + + public const ALLOW_PRIVATE = 1; + public const ALLOW_PROTECTED = 2; + public const ALLOW_PUBLIC = 4; + + /** @var int Allow none of the magic methods */ + public const DISALLOW_MAGIC_METHODS = 0; + /** @var int Allow magic __get methods */ + public const ALLOW_MAGIC_GET = 1 << 0; + /** @var int Allow magic __set methods */ + public const ALLOW_MAGIC_SET = 1 << 1; + /** @var int Allow magic __call methods */ + public const ALLOW_MAGIC_CALL = 1 << 2; + + private const MAP_TYPES = [ + 'integer' => Type::BUILTIN_TYPE_INT, + 'boolean' => Type::BUILTIN_TYPE_BOOL, + 'double' => Type::BUILTIN_TYPE_FLOAT, + ]; + + private array $mutatorPrefixes; + private array $accessorPrefixes; + private array $arrayMutatorPrefixes; + private bool $enableConstructorExtraction; + private int $methodReflectionFlags; + private int $magicMethodsFlags; + private int $propertyReflectionFlags; + private InflectorInterface $inflector; + private array $arrayMutatorPrefixesFirst; + private array $arrayMutatorPrefixesLast; + + /** + * @param string[]|null $mutatorPrefixes + * @param string[]|null $accessorPrefixes + * @param string[]|null $arrayMutatorPrefixes + */ + public function __construct(?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, ?InflectorInterface $inflector = null, int $magicMethodsFlags = self::ALLOW_MAGIC_GET | self::ALLOW_MAGIC_SET) + { + $this->mutatorPrefixes = $mutatorPrefixes ?? self::$defaultMutatorPrefixes; + $this->accessorPrefixes = $accessorPrefixes ?? self::$defaultAccessorPrefixes; + $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? self::$defaultArrayMutatorPrefixes; + $this->enableConstructorExtraction = $enableConstructorExtraction; + $this->methodReflectionFlags = $this->getMethodsFlags($accessFlags); + $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags); + $this->magicMethodsFlags = $magicMethodsFlags; + $this->inflector = $inflector ?? new EnglishInflector(); + + $this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes)); + $this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst); + } + + public function getProperties(string $class, array $context = []): ?array + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + $reflectionProperties = $reflectionClass->getProperties(); + + $properties = []; + foreach ($reflectionProperties as $reflectionProperty) { + if ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags) { + $properties[$reflectionProperty->name] = $reflectionProperty->name; + } + } + + foreach ($reflectionClass->getMethods($this->methodReflectionFlags) as $reflectionMethod) { + if ($reflectionMethod->isStatic()) { + continue; + } + + $propertyName = $this->getPropertyName($reflectionMethod->name, $reflectionProperties); + if (!$propertyName || isset($properties[$propertyName])) { + continue; + } + if ($reflectionClass->hasProperty($lowerCasedPropertyName = lcfirst($propertyName)) || (!$reflectionClass->hasProperty($propertyName) && !preg_match('/^[A-Z]{2,}/', $propertyName))) { + $propertyName = $lowerCasedPropertyName; + } + $properties[$propertyName] = $propertyName; + } + + return $properties ? array_values($properties) : null; + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + if ($fromMutator = $this->extractFromMutator($class, $property)) { + return $fromMutator; + } + + if ($fromAccessor = $this->extractFromAccessor($class, $property)) { + return $fromAccessor; + } + + if ( + ($context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction) + && $fromConstructor = $this->extractFromConstructor($class, $property) + ) { + return $fromConstructor; + } + + if ($fromPropertyDeclaration = $this->extractFromPropertyDeclaration($class, $property)) { + return $fromPropertyDeclaration; + } + + return null; + } + + public function getTypesFromConstructor(string $class, string $property): ?array + { + try { + $reflection = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + if (!$reflectionConstructor = $reflection->getConstructor()) { + return null; + } + if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) { + return null; + } + if (!$reflectionType = $reflectionParameter->getType()) { + return null; + } + if (!$types = $this->extractFromReflectionType($reflectionType, $reflectionConstructor->getDeclaringClass())) { + return null; + } + + return $types; + } + + private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter + { + foreach ($reflectionConstructor->getParameters() as $reflectionParameter) { + if ($reflectionParameter->getName() === $property) { + return $reflectionParameter; + } + } + + return null; + } + + public function isReadable(string $class, string $property, array $context = []): ?bool + { + if ($this->isAllowedProperty($class, $property)) { + return true; + } + + return null !== $this->getReadInfo($class, $property, $context); + } + + public function isWritable(string $class, string $property, array $context = []): ?bool + { + if ($this->isAllowedProperty($class, $property, true)) { + return true; + } + + // First test with the camelized property name + [$reflectionMethod] = $this->getMutatorMethod($class, $this->camelize($property)); + if (null !== $reflectionMethod) { + return true; + } + + // Otherwise check for the old way + [$reflectionMethod] = $this->getMutatorMethod($class, $property); + + return null !== $reflectionMethod; + } + + public function isInitializable(string $class, string $property, array $context = []): ?bool + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + if (!$reflectionClass->isInstantiable()) { + return false; + } + + if ($constructor = $reflectionClass->getConstructor()) { + foreach ($constructor->getParameters() as $parameter) { + if ($property === $parameter->name) { + return true; + } + } + } elseif ($parentClass = $reflectionClass->getParentClass()) { + return $this->isInitializable($parentClass->getName(), $property); + } + + return false; + } + + public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo + { + try { + $reflClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false; + $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags; + $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL); + $allowMagicGet = (bool) ($magicMethods & self::ALLOW_MAGIC_GET); + $hasProperty = $reflClass->hasProperty($property); + $camelProp = $this->camelize($property); + $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item) + + foreach ($this->accessorPrefixes as $prefix) { + $methodName = $prefix.$camelProp; + + if ($reflClass->hasMethod($methodName) && $reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags && !$reflClass->getMethod($methodName)->getNumberOfRequiredParameters()) { + $method = $reflClass->getMethod($methodName); + + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisiblityForMethod($method), $method->isStatic(), false); + } + } + + if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) { + $method = $reflClass->getMethod($getsetter); + + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic(), false); + } + + if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference()); + } + + if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($r), $r->isStatic(), true); + } + + if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, 'get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); + } + + return null; + } + + public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo + { + try { + $reflClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false; + $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags; + $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL); + $allowMagicSet = (bool) ($magicMethods & self::ALLOW_MAGIC_SET); + $allowConstruct = $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction; + $allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true; + + $camelized = $this->camelize($property); + $constructor = $reflClass->getConstructor(); + $singulars = $this->inflector->singularize($camelized); + $errors = []; + + if (null !== $constructor && $allowConstruct) { + foreach ($constructor->getParameters() as $parameter) { + if ($parameter->getName() === $property) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_CONSTRUCTOR, $property); + } + } + } + + [$adderAccessName, $removerAccessName, $adderAndRemoverErrors] = $this->findAdderAndRemover($reflClass, $singulars); + if ($allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { + $adderMethod = $reflClass->getMethod($adderAccessName); + $removerMethod = $reflClass->getMethod($removerAccessName); + + $mutator = new PropertyWriteInfo(PropertyWriteInfo::TYPE_ADDER_AND_REMOVER); + $mutator->setAdderInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $adderAccessName, $this->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic())); + $mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisiblityForMethod($removerMethod), $removerMethod->isStatic())); + + return $mutator; + } + + $errors[] = $adderAndRemoverErrors; + + foreach ($this->mutatorPrefixes as $mutatorPrefix) { + $methodName = $mutatorPrefix.$camelized; + + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1); + if (!$accessible) { + $errors[] = $methodAccessibleErrors; + continue; + } + + $method = $reflClass->getMethod($methodName); + + if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + } + } + + $getsetter = lcfirst($camelized); + + if ($allowGetterSetter) { + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1); + if ($accessible) { + $method = $reflClass->getMethod($getsetter); + + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetter, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + } + + $errors[] = $methodAccessibleErrors; + } + + if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { + $reflProperty = $reflClass->getProperty($property); + if (!$reflProperty->isReadOnly()) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic()); + } + + $errors[] = [\sprintf('The property "%s" in class "%s" is a promoted readonly property.', $property, $reflClass->getName())]; + $allowMagicSet = $allowMagicCall = false; + } + + if ($allowMagicSet) { + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2); + if ($accessible) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + } + + $errors[] = $methodAccessibleErrors; + } + + if ($allowMagicCall) { + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2); + if ($accessible) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + } + + $errors[] = $methodAccessibleErrors; + } + + if (!$allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { + $errors[] = [\sprintf( + 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. + 'the new value must be an array or an instance of \Traversable', + $property, + $reflClass->getName(), + implode('()", "', [$adderAccessName, $removerAccessName]) + )]; + } + + $noneProperty = new PropertyWriteInfo(); + $noneProperty->setErrors(array_merge([], ...$errors)); + + return $noneProperty; + } + + /** + * @return Type[]|null + */ + private function extractFromMutator(string $class, string $property): ?array + { + [$reflectionMethod, $prefix] = $this->getMutatorMethod($class, $property); + if (null === $reflectionMethod) { + return null; + } + + $reflectionParameters = $reflectionMethod->getParameters(); + $reflectionParameter = $reflectionParameters[0]; + + if (!$reflectionType = $reflectionParameter->getType()) { + return null; + } + $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass()); + + if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) { + $type = [new Type(Type::BUILTIN_TYPE_ARRAY, $this->isNullableProperty($class, $property), null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])]; + } + + return $type; + } + + /** + * Tries to extract type information from accessors. + * + * @return Type[]|null + */ + private function extractFromAccessor(string $class, string $property): ?array + { + [$reflectionMethod, $prefix] = $this->getAccessorMethod($class, $property); + if (null === $reflectionMethod) { + return null; + } + + if ($reflectionType = $reflectionMethod->getReturnType()) { + return $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass()); + } + + if (\in_array($prefix, ['is', 'can', 'has'])) { + return [new Type(Type::BUILTIN_TYPE_BOOL)]; + } + + return null; + } + + /** + * Tries to extract type information from constructor. + * + * @return Type[]|null + */ + private function extractFromConstructor(string $class, string $property): ?array + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + $constructor = $reflectionClass->getConstructor(); + + if (!$constructor) { + return null; + } + + foreach ($constructor->getParameters() as $parameter) { + if ($property !== $parameter->name) { + continue; + } + $reflectionType = $parameter->getType(); + + return $reflectionType ? $this->extractFromReflectionType($reflectionType, $constructor->getDeclaringClass()) : null; + } + + if ($parentClass = $reflectionClass->getParentClass()) { + return $this->extractFromConstructor($parentClass->getName(), $property); + } + + return null; + } + + private function extractFromPropertyDeclaration(string $class, string $property): ?array + { + try { + $reflectionClass = new \ReflectionClass($class); + + $reflectionProperty = $reflectionClass->getProperty($property); + $reflectionPropertyType = $reflectionProperty->getType(); + + if (null !== $reflectionPropertyType && $types = $this->extractFromReflectionType($reflectionPropertyType, $reflectionProperty->getDeclaringClass())) { + return $types; + } + } catch (\ReflectionException) { + return null; + } + + $defaultValue = $reflectionClass->getDefaultProperties()[$property] ?? null; + + if (null === $defaultValue) { + return null; + } + + $type = \gettype($defaultValue); + $type = static::MAP_TYPES[$type] ?? $type; + + return [new Type($type, $this->isNullableProperty($class, $property), null, Type::BUILTIN_TYPE_ARRAY === $type)]; + } + + private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array + { + $types = []; + $nullable = $reflectionType->allowsNull(); + + foreach (($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) ? $reflectionType->getTypes() : [$reflectionType] as $type) { + if (!$type instanceof \ReflectionNamedType) { + // Nested composite types are not supported yet. + return []; + } + + $phpTypeOrClass = $type->getName(); + if ('null' === $phpTypeOrClass || 'mixed' === $phpTypeOrClass || 'never' === $phpTypeOrClass) { + continue; + } + + if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) { + $types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true); + } elseif ('void' === $phpTypeOrClass) { + $types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable); + } elseif ($type->isBuiltin()) { + $types[] = new Type($phpTypeOrClass, $nullable); + } else { + $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $declaringClass)); + } + } + + return $types; + } + + private function resolveTypeName(string $name, \ReflectionClass $declaringClass): string + { + if ('self' === $lcName = strtolower($name)) { + return $declaringClass->name; + } + if ('parent' === $lcName && $parent = $declaringClass->getParentClass()) { + return $parent->name; + } + + return $name; + } + + private function isNullableProperty(string $class, string $property): bool + { + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + + $reflectionPropertyType = $reflectionProperty->getType(); + + return null !== $reflectionPropertyType && $reflectionPropertyType->allowsNull(); + } catch (\ReflectionException) { + // Return false if the property doesn't exist + } + + return false; + } + + private function isAllowedProperty(string $class, string $property, bool $writeAccessRequired = false): bool + { + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + + if ($writeAccessRequired) { + if ($reflectionProperty->isReadOnly()) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isProtectedSet()) { + return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PROTECTED); + } + + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isPrivateSet()) { + return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PRIVATE); + } + + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { + return false; + } + } + + return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags); + } catch (\ReflectionException) { + // Return false if the property doesn't exist + } + + return false; + } + + /** + * Gets the accessor method. + * + * Returns an array with a the instance of \ReflectionMethod as first key + * and the prefix of the method as second or null if not found. + */ + private function getAccessorMethod(string $class, string $property): ?array + { + $ucProperty = ucfirst($property); + + foreach ($this->accessorPrefixes as $prefix) { + try { + $reflectionMethod = new \ReflectionMethod($class, $prefix.$ucProperty); + if ($reflectionMethod->isStatic()) { + continue; + } + + if (0 === $reflectionMethod->getNumberOfRequiredParameters()) { + return [$reflectionMethod, $prefix]; + } + } catch (\ReflectionException) { + // Return null if the property doesn't exist + } + } + + return null; + } + + /** + * Returns an array with a the instance of \ReflectionMethod as first key + * and the prefix of the method as second or null if not found. + */ + private function getMutatorMethod(string $class, string $property): ?array + { + $ucProperty = ucfirst($property); + $ucSingulars = $this->inflector->singularize($ucProperty); + + $mutatorPrefixes = \in_array($ucProperty, $ucSingulars, true) ? $this->arrayMutatorPrefixesLast : $this->arrayMutatorPrefixesFirst; + + foreach ($mutatorPrefixes as $prefix) { + $names = [$ucProperty]; + if (\in_array($prefix, $this->arrayMutatorPrefixes)) { + $names = array_merge($names, $ucSingulars); + } + + foreach ($names as $name) { + try { + $reflectionMethod = new \ReflectionMethod($class, $prefix.$name); + if ($reflectionMethod->isStatic()) { + continue; + } + + // Parameter can be optional to allow things like: method(?array $foo = null) + if ($reflectionMethod->getNumberOfParameters() >= 1) { + return [$reflectionMethod, $prefix]; + } + } catch (\ReflectionException) { + // Try the next prefix if the method doesn't exist + } + } + } + + return null; + } + + private function getPropertyName(string $methodName, array $reflectionProperties): ?string + { + $pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes)); + + if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) { + if (!\in_array($matches[1], $this->arrayMutatorPrefixes)) { + return $matches[2]; + } + + foreach ($reflectionProperties as $reflectionProperty) { + foreach ($this->inflector->singularize($reflectionProperty->name) as $name) { + if (strtolower($name) === strtolower($matches[2])) { + return $reflectionProperty->name; + } + } + } + + return $matches[2]; + } + + return null; + } + + /** + * Searches for add and remove methods. + * + * @param \ReflectionClass $reflClass The reflection class for the given object + * @param array $singulars The singular form of the property name or null + * + * @return array An array containing the adder and remover when found and errors + */ + private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): array + { + if (!\is_array($this->arrayMutatorPrefixes) && 2 !== \count($this->arrayMutatorPrefixes)) { + return [null, null, []]; + } + + [$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes; + $errors = []; + + foreach ($singulars as $singular) { + $addMethod = $addPrefix.$singular; + $removeMethod = $removePrefix.$singular; + + [$addMethodFound, $addMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $addMethod, 1); + [$removeMethodFound, $removeMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $removeMethod, 1); + $errors[] = $addMethodAccessibleErrors; + $errors[] = $removeMethodAccessibleErrors; + + if ($addMethodFound && $removeMethodFound) { + return [$addMethod, $removeMethod, []]; + } + + if ($addMethodFound && !$removeMethodFound) { + $errors[] = [\sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $addMethod, $reflClass->getName(), $removeMethod)]; + } elseif (!$addMethodFound && $removeMethodFound) { + $errors[] = [\sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $removeMethod, $reflClass->getName(), $addMethod)]; + } + } + + return [null, null, array_merge([], ...$errors)]; + } + + /** + * Returns whether a method is public and has the number of required parameters and errors. + */ + private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): array + { + $errors = []; + + if ($class->hasMethod($methodName)) { + $method = $class->getMethod($methodName); + + if (\ReflectionMethod::IS_PUBLIC === $this->methodReflectionFlags && !$method->isPublic()) { + $errors[] = \sprintf('The method "%s" in class "%s" was found but does not have public access.', $methodName, $class->getName()); + } elseif ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) { + $errors[] = \sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d.', $methodName, $class->getName(), $method->getNumberOfRequiredParameters(), $parameters); + } else { + return [true, $errors]; + } + } + + return [false, $errors]; + } + + /** + * Camelizes a given string. + */ + private function camelize(string $string): string + { + return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); + } + + /** + * Return allowed reflection method flags. + */ + private function getMethodsFlags(int $accessFlags): int + { + $methodFlags = 0; + + if ($accessFlags & self::ALLOW_PUBLIC) { + $methodFlags |= \ReflectionMethod::IS_PUBLIC; + } + + if ($accessFlags & self::ALLOW_PRIVATE) { + $methodFlags |= \ReflectionMethod::IS_PRIVATE; + } + + if ($accessFlags & self::ALLOW_PROTECTED) { + $methodFlags |= \ReflectionMethod::IS_PROTECTED; + } + + return $methodFlags; + } + + /** + * Return allowed reflection property flags. + */ + private function getPropertyFlags(int $accessFlags): int + { + $propertyFlags = 0; + + if ($accessFlags & self::ALLOW_PUBLIC) { + $propertyFlags |= \ReflectionProperty::IS_PUBLIC; + } + + if ($accessFlags & self::ALLOW_PRIVATE) { + $propertyFlags |= \ReflectionProperty::IS_PRIVATE; + } + + if ($accessFlags & self::ALLOW_PROTECTED) { + $propertyFlags |= \ReflectionProperty::IS_PROTECTED; + } + + return $propertyFlags; + } + + private function getReadVisiblityForProperty(\ReflectionProperty $reflectionProperty): string + { + if ($reflectionProperty->isPrivate()) { + return PropertyReadInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isProtected()) { + return PropertyReadInfo::VISIBILITY_PROTECTED; + } + + return PropertyReadInfo::VISIBILITY_PUBLIC; + } + + private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): string + { + if ($reflectionMethod->isPrivate()) { + return PropertyReadInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionMethod->isProtected()) { + return PropertyReadInfo::VISIBILITY_PROTECTED; + } + + return PropertyReadInfo::VISIBILITY_PUBLIC; + } + + private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string + { + if (\PHP_VERSION_ID >= 80400) { + if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isPrivateSet()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isProtectedSet()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + } + + if ($reflectionProperty->isPrivate()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isProtected()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + + return PropertyWriteInfo::VISIBILITY_PUBLIC; + } + + private function getWriteVisiblityForMethod(\ReflectionMethod $reflectionMethod): string + { + if ($reflectionMethod->isPrivate()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionMethod->isProtected()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + + return PropertyWriteInfo::VISIBILITY_PUBLIC; + } +} diff --git a/lib/symfony/property-info/Extractor/SerializerExtractor.php b/lib/symfony/property-info/Extractor/SerializerExtractor.php new file mode 100644 index 0000000000..0445b0be9a --- /dev/null +++ b/lib/symfony/property-info/Extractor/SerializerExtractor.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * Lists available properties using Symfony Serializer Component metadata. + * + * @author Kévin Dunglas + * + * @final + */ +class SerializerExtractor implements PropertyListExtractorInterface +{ + public function __construct( + private readonly ClassMetadataFactoryInterface $classMetadataFactory, + ) { + } + + public function getProperties(string $class, array $context = []): ?array + { + if (!\array_key_exists('serializer_groups', $context) || (null !== $context['serializer_groups'] && !\is_array($context['serializer_groups']))) { + return null; + } + + if (!$this->classMetadataFactory->hasMetadataFor($class)) { + return null; + } + + $properties = []; + $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); + + foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { + if (!$serializerAttributeMetadata->isIgnored() && (null === $context['serializer_groups'] || array_intersect($context['serializer_groups'], $serializerAttributeMetadata->getGroups()))) { + $properties[] = $serializerAttributeMetadata->getName(); + } + } + + return $properties; + } +} diff --git a/lib/symfony/property-info/LICENSE b/lib/symfony/property-info/LICENSE new file mode 100644 index 0000000000..6e3afce692 --- /dev/null +++ b/lib/symfony/property-info/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/property-info/PhpStan/NameScope.php b/lib/symfony/property-info/PhpStan/NameScope.php new file mode 100644 index 0000000000..3a913b9f55 --- /dev/null +++ b/lib/symfony/property-info/PhpStan/NameScope.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\PropertyInfo\PhpStan; + +/** + * NameScope class adapted from PHPStan code. + * + * @copyright Copyright (c) 2016, PHPStan https://github.com/phpstan/phpstan-src + * @copyright Copyright (c) 2016, Ondřej Mirtes + * @author Baptiste Leduc + * + * @internal + */ +final class NameScope +{ + private string $calledClassName; + private string $namespace; + /** @var array alias(string) => fullName(string) */ + private array $uses; + + public function __construct(string $calledClassName, string $namespace, array $uses = []) + { + $this->calledClassName = $calledClassName; + $this->namespace = $namespace; + $this->uses = $uses; + } + + public function resolveStringName(string $name): string + { + if (str_starts_with($name, '\\')) { + return ltrim($name, '\\'); + } + + $nameParts = explode('\\', $name); + $firstNamePart = $nameParts[0]; + if (isset($this->uses[$firstNamePart])) { + if (1 === \count($nameParts)) { + return $this->uses[$firstNamePart]; + } + array_shift($nameParts); + + return \sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts)); + } + + if (null !== $this->namespace) { + return \sprintf('%s\\%s', $this->namespace, $name); + } + + return $name; + } + + public function resolveRootClass(): string + { + return $this->resolveStringName($this->calledClassName); + } +} diff --git a/lib/symfony/property-info/PhpStan/NameScopeFactory.php b/lib/symfony/property-info/PhpStan/NameScopeFactory.php new file mode 100644 index 0000000000..1cd74faa5c --- /dev/null +++ b/lib/symfony/property-info/PhpStan/NameScopeFactory.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\PhpStan; + +use phpDocumentor\Reflection\Types\ContextFactory; + +/** + * @author Baptiste Leduc + * + * @internal + */ +final class NameScopeFactory +{ + public function create(string $calledClassName, ?string $declaringClassName = null): NameScope + { + $declaringClassName ??= $calledClassName; + + $path = explode('\\', $calledClassName); + $calledClassName = array_pop($path); + + $declaringReflection = new \ReflectionClass($declaringClassName); + [$declaringNamespace, $declaringUses] = $this->extractFromFullClassName($declaringReflection); + $declaringUses = array_merge($declaringUses, $this->collectUses($declaringReflection)); + + return new NameScope($calledClassName, $declaringNamespace, $declaringUses); + } + + private function collectUses(\ReflectionClass $reflection): array + { + $uses = [$this->extractFromFullClassName($reflection)[1]]; + + foreach ($reflection->getTraits() as $traitReflection) { + $uses[] = $this->extractFromFullClassName($traitReflection)[1]; + } + + if (false !== $parentClass = $reflection->getParentClass()) { + $uses[] = $this->collectUses($parentClass); + } + + return $uses ? array_merge(...$uses) : []; + } + + private function extractFromFullClassName(\ReflectionClass $reflection): array + { + $namespace = trim($reflection->getNamespaceName(), '\\'); + $fileName = $reflection->getFileName(); + + if (\is_string($fileName) && is_file($fileName)) { + if (false === $contents = file_get_contents($fileName)) { + throw new \RuntimeException(\sprintf('Unable to read file "%s".', $fileName)); + } + + $factory = new ContextFactory(); + $context = $factory->createForNamespace($namespace, $contents); + + return [$namespace, $context->getNamespaceAliases()]; + } + + return [$namespace, []]; + } +} diff --git a/lib/symfony/property-info/PropertyAccessExtractorInterface.php b/lib/symfony/property-info/PropertyAccessExtractorInterface.php new file mode 100644 index 0000000000..f9ee787130 --- /dev/null +++ b/lib/symfony/property-info/PropertyAccessExtractorInterface.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\PropertyInfo; + +/** + * Guesses if the property can be accessed or mutated. + * + * @author Kévin Dunglas + */ +interface PropertyAccessExtractorInterface +{ + /** + * Is the property readable? + * + * @return bool|null + */ + public function isReadable(string $class, string $property, array $context = []); + + /** + * Is the property writable? + * + * @return bool|null + */ + public function isWritable(string $class, string $property, array $context = []); +} diff --git a/lib/symfony/property-info/PropertyDescriptionExtractorInterface.php b/lib/symfony/property-info/PropertyDescriptionExtractorInterface.php new file mode 100644 index 0000000000..a779d159cd --- /dev/null +++ b/lib/symfony/property-info/PropertyDescriptionExtractorInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Guesses the property's human readable description. + * + * @author Kévin Dunglas + */ +interface PropertyDescriptionExtractorInterface +{ + /** + * Gets the short description of the property. + */ + public function getShortDescription(string $class, string $property, array $context = []): ?string; + + /** + * Gets the long description of the property. + */ + public function getLongDescription(string $class, string $property, array $context = []): ?string; +} diff --git a/lib/symfony/property-info/PropertyInfoCacheExtractor.php b/lib/symfony/property-info/PropertyInfoCacheExtractor.php new file mode 100644 index 0000000000..b4543eace7 --- /dev/null +++ b/lib/symfony/property-info/PropertyInfoCacheExtractor.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * Adds a PSR-6 cache layer on top of an extractor. + * + * @author Kévin Dunglas + * + * @final + */ +class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface +{ + private array $arrayCache = []; + + public function __construct( + private readonly PropertyInfoExtractorInterface $propertyInfoExtractor, + private readonly CacheItemPoolInterface $cacheItemPool, + ) { + } + + public function isReadable(string $class, string $property, array $context = []): ?bool + { + return $this->extract('isReadable', [$class, $property, $context]); + } + + public function isWritable(string $class, string $property, array $context = []): ?bool + { + return $this->extract('isWritable', [$class, $property, $context]); + } + + public function getShortDescription(string $class, string $property, array $context = []): ?string + { + return $this->extract('getShortDescription', [$class, $property, $context]); + } + + public function getLongDescription(string $class, string $property, array $context = []): ?string + { + return $this->extract('getLongDescription', [$class, $property, $context]); + } + + public function getProperties(string $class, array $context = []): ?array + { + return $this->extract('getProperties', [$class, $context]); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return $this->extract('getTypes', [$class, $property, $context]); + } + + public function isInitializable(string $class, string $property, array $context = []): ?bool + { + return $this->extract('isInitializable', [$class, $property, $context]); + } + + /** + * Retrieves the cached data if applicable or delegates to the decorated extractor. + */ + private function extract(string $method, array $arguments): mixed + { + try { + $serializedArguments = serialize($arguments); + } catch (\Exception) { + // If arguments are not serializable, skip the cache + return $this->propertyInfoExtractor->{$method}(...$arguments); + } + + // Calling rawurlencode escapes special characters not allowed in PSR-6's keys + $key = rawurlencode($method.'.'.$serializedArguments); + + if (\array_key_exists($key, $this->arrayCache)) { + return $this->arrayCache[$key]; + } + + $item = $this->cacheItemPool->getItem($key); + + if ($item->isHit()) { + return $this->arrayCache[$key] = $item->get(); + } + + $value = $this->propertyInfoExtractor->{$method}(...$arguments); + $item->set($value); + $this->cacheItemPool->save($item); + + return $this->arrayCache[$key] = $value; + } +} diff --git a/lib/symfony/property-info/PropertyInfoExtractor.php b/lib/symfony/property-info/PropertyInfoExtractor.php new file mode 100644 index 0000000000..7416849a0a --- /dev/null +++ b/lib/symfony/property-info/PropertyInfoExtractor.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Default {@see PropertyInfoExtractorInterface} implementation. + * + * @author Kévin Dunglas + * + * @final + */ +class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface +{ + /** + * @param iterable $listExtractors + * @param iterable $typeExtractors + * @param iterable $descriptionExtractors + * @param iterable $accessExtractors + * @param iterable $initializableExtractors + */ + public function __construct( + private readonly iterable $listExtractors = [], + private readonly iterable $typeExtractors = [], + private readonly iterable $descriptionExtractors = [], + private readonly iterable $accessExtractors = [], + private readonly iterable $initializableExtractors = [], + ) { + } + + public function getProperties(string $class, array $context = []): ?array + { + return $this->extract($this->listExtractors, 'getProperties', [$class, $context]); + } + + public function getShortDescription(string $class, string $property, array $context = []): ?string + { + return $this->extract($this->descriptionExtractors, 'getShortDescription', [$class, $property, $context]); + } + + public function getLongDescription(string $class, string $property, array $context = []): ?string + { + return $this->extract($this->descriptionExtractors, 'getLongDescription', [$class, $property, $context]); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return $this->extract($this->typeExtractors, 'getTypes', [$class, $property, $context]); + } + + public function isReadable(string $class, string $property, array $context = []): ?bool + { + return $this->extract($this->accessExtractors, 'isReadable', [$class, $property, $context]); + } + + public function isWritable(string $class, string $property, array $context = []): ?bool + { + return $this->extract($this->accessExtractors, 'isWritable', [$class, $property, $context]); + } + + public function isInitializable(string $class, string $property, array $context = []): ?bool + { + return $this->extract($this->initializableExtractors, 'isInitializable', [$class, $property, $context]); + } + + /** + * Iterates over registered extractors and return the first value found. + * + * @param iterable $extractors + * @param list $arguments + */ + private function extract(iterable $extractors, string $method, array $arguments): mixed + { + foreach ($extractors as $extractor) { + if (null !== $value = $extractor->{$method}(...$arguments)) { + return $value; + } + } + + return null; + } +} diff --git a/lib/symfony/property-info/PropertyInfoExtractorInterface.php b/lib/symfony/property-info/PropertyInfoExtractorInterface.php new file mode 100644 index 0000000000..8893018653 --- /dev/null +++ b/lib/symfony/property-info/PropertyInfoExtractorInterface.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\PropertyInfo; + +/** + * Gets info about PHP class properties. + * + * A convenient interface inheriting all specific info interfaces. + * + * @author Kévin Dunglas + */ +interface PropertyInfoExtractorInterface extends PropertyTypeExtractorInterface, PropertyDescriptionExtractorInterface, PropertyAccessExtractorInterface, PropertyListExtractorInterface +{ +} diff --git a/lib/symfony/property-info/PropertyInitializableExtractorInterface.php b/lib/symfony/property-info/PropertyInitializableExtractorInterface.php new file mode 100644 index 0000000000..13248fc194 --- /dev/null +++ b/lib/symfony/property-info/PropertyInitializableExtractorInterface.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\PropertyInfo; + +/** + * Guesses if the property can be initialized through the constructor. + * + * @author Kévin Dunglas + */ +interface PropertyInitializableExtractorInterface +{ + /** + * Is the property initializable? Returns true if a constructor's parameter matches the given property name. + */ + public function isInitializable(string $class, string $property, array $context = []): ?bool; +} diff --git a/lib/symfony/property-info/PropertyListExtractorInterface.php b/lib/symfony/property-info/PropertyListExtractorInterface.php new file mode 100644 index 0000000000..326e6cccb3 --- /dev/null +++ b/lib/symfony/property-info/PropertyListExtractorInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Extracts the list of properties available for the given class. + * + * @author Kévin Dunglas + */ +interface PropertyListExtractorInterface +{ + /** + * Gets the list of properties available for the given class. + * + * @return string[]|null + */ + public function getProperties(string $class, array $context = []); +} diff --git a/lib/symfony/property-info/PropertyReadInfo.php b/lib/symfony/property-info/PropertyReadInfo.php new file mode 100644 index 0000000000..d006e32483 --- /dev/null +++ b/lib/symfony/property-info/PropertyReadInfo.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * The property read info tells how a property can be read. + * + * @author Joel Wurtz + */ +final class PropertyReadInfo +{ + public const TYPE_METHOD = 'method'; + public const TYPE_PROPERTY = 'property'; + + public const VISIBILITY_PUBLIC = 'public'; + public const VISIBILITY_PROTECTED = 'protected'; + public const VISIBILITY_PRIVATE = 'private'; + + public function __construct( + private readonly string $type, + private readonly string $name, + private readonly string $visibility, + private readonly bool $static, + private readonly bool $byRef, + ) { + } + + /** + * Get type of access. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get name of the access, which can be a method name or a property name, depending on the type. + */ + public function getName(): string + { + return $this->name; + } + + public function getVisibility(): string + { + return $this->visibility; + } + + public function isStatic(): bool + { + return $this->static; + } + + /** + * Whether this accessor can be accessed by reference. + */ + public function canBeReference(): bool + { + return $this->byRef; + } +} diff --git a/lib/symfony/property-info/PropertyReadInfoExtractorInterface.php b/lib/symfony/property-info/PropertyReadInfoExtractorInterface.php new file mode 100644 index 0000000000..816b2825d5 --- /dev/null +++ b/lib/symfony/property-info/PropertyReadInfoExtractorInterface.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\PropertyInfo; + +/** + * Extract read information for the property of a class. + * + * @author Joel Wurtz + */ +interface PropertyReadInfoExtractorInterface +{ + /** + * Get read information object for a given property of a class. + */ + public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo; +} diff --git a/lib/symfony/property-info/PropertyTypeExtractorInterface.php b/lib/symfony/property-info/PropertyTypeExtractorInterface.php new file mode 100644 index 0000000000..6da0bcb4c8 --- /dev/null +++ b/lib/symfony/property-info/PropertyTypeExtractorInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Type Extractor Interface. + * + * @author Kévin Dunglas + */ +interface PropertyTypeExtractorInterface +{ + /** + * Gets types of a property. + * + * @return Type[]|null + */ + public function getTypes(string $class, string $property, array $context = []); +} diff --git a/lib/symfony/property-info/PropertyWriteInfo.php b/lib/symfony/property-info/PropertyWriteInfo.php new file mode 100644 index 0000000000..81ce7eda6d --- /dev/null +++ b/lib/symfony/property-info/PropertyWriteInfo.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * The write mutator defines how a property can be written. + * + * @author Joel Wurtz + */ +final class PropertyWriteInfo +{ + public const TYPE_NONE = 'none'; + public const TYPE_METHOD = 'method'; + public const TYPE_PROPERTY = 'property'; + public const TYPE_ADDER_AND_REMOVER = 'adder_and_remover'; + public const TYPE_CONSTRUCTOR = 'constructor'; + + public const VISIBILITY_PUBLIC = 'public'; + public const VISIBILITY_PROTECTED = 'protected'; + public const VISIBILITY_PRIVATE = 'private'; + + private ?self $adderInfo = null; + private ?self $removerInfo = null; + private array $errors = []; + + public function __construct( + private readonly string $type = self::TYPE_NONE, + private readonly ?string $name = null, + private readonly ?string $visibility = null, + private readonly ?bool $static = null, + ) { + } + + public function getType(): string + { + return $this->type; + } + + public function getName(): string + { + if (null === $this->name) { + throw new \LogicException("Calling getName() when having a mutator of type {$this->type} is not tolerated."); + } + + return $this->name; + } + + public function setAdderInfo(self $adderInfo): void + { + $this->adderInfo = $adderInfo; + } + + public function getAdderInfo(): self + { + if (null === $this->adderInfo) { + throw new \LogicException("Calling getAdderInfo() when having a mutator of type {$this->type} is not tolerated."); + } + + return $this->adderInfo; + } + + public function setRemoverInfo(self $removerInfo): void + { + $this->removerInfo = $removerInfo; + } + + public function getRemoverInfo(): self + { + if (null === $this->removerInfo) { + throw new \LogicException("Calling getRemoverInfo() when having a mutator of type {$this->type} is not tolerated."); + } + + return $this->removerInfo; + } + + public function getVisibility(): string + { + if (null === $this->visibility) { + throw new \LogicException("Calling getVisibility() when having a mutator of type {$this->type} is not tolerated."); + } + + return $this->visibility; + } + + public function isStatic(): bool + { + if (null === $this->static) { + throw new \LogicException("Calling isStatic() when having a mutator of type {$this->type} is not tolerated."); + } + + return $this->static; + } + + public function setErrors(array $errors): void + { + $this->errors = $errors; + } + + public function getErrors(): array + { + return $this->errors; + } + + public function hasErrors(): bool + { + return (bool) \count($this->errors); + } +} diff --git a/lib/symfony/property-info/PropertyWriteInfoExtractorInterface.php b/lib/symfony/property-info/PropertyWriteInfoExtractorInterface.php new file mode 100644 index 0000000000..f113463818 --- /dev/null +++ b/lib/symfony/property-info/PropertyWriteInfoExtractorInterface.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\PropertyInfo; + +/** + * Extract write information for the property of a class. + * + * @author Joel Wurtz + */ +interface PropertyWriteInfoExtractorInterface +{ + /** + * Get write information object for a given property of a class. + */ + public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo; +} diff --git a/lib/symfony/property-info/README.md b/lib/symfony/property-info/README.md new file mode 100644 index 0000000000..da3514fc9d --- /dev/null +++ b/lib/symfony/property-info/README.md @@ -0,0 +1,14 @@ +PropertyInfo Component +====================== + +The PropertyInfo component extracts information about PHP class' properties +using metadata of popular sources. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/property_info.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/lib/symfony/property-info/Type.php b/lib/symfony/property-info/Type.php new file mode 100644 index 0000000000..e9bf15e0d2 --- /dev/null +++ b/lib/symfony/property-info/Type.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Type value object (immutable). + * + * @author Kévin Dunglas + * + * @final + */ +class Type +{ + public const BUILTIN_TYPE_INT = 'int'; + public const BUILTIN_TYPE_FLOAT = 'float'; + public const BUILTIN_TYPE_STRING = 'string'; + public const BUILTIN_TYPE_BOOL = 'bool'; + public const BUILTIN_TYPE_RESOURCE = 'resource'; + public const BUILTIN_TYPE_OBJECT = 'object'; + public const BUILTIN_TYPE_ARRAY = 'array'; + public const BUILTIN_TYPE_NULL = 'null'; + public const BUILTIN_TYPE_FALSE = 'false'; + public const BUILTIN_TYPE_TRUE = 'true'; + public const BUILTIN_TYPE_CALLABLE = 'callable'; + public const BUILTIN_TYPE_ITERABLE = 'iterable'; + + /** + * List of PHP builtin types. + * + * @var string[] + */ + public static $builtinTypes = [ + self::BUILTIN_TYPE_INT, + self::BUILTIN_TYPE_FLOAT, + self::BUILTIN_TYPE_STRING, + self::BUILTIN_TYPE_BOOL, + self::BUILTIN_TYPE_RESOURCE, + self::BUILTIN_TYPE_OBJECT, + self::BUILTIN_TYPE_ARRAY, + self::BUILTIN_TYPE_CALLABLE, + self::BUILTIN_TYPE_FALSE, + self::BUILTIN_TYPE_TRUE, + self::BUILTIN_TYPE_NULL, + self::BUILTIN_TYPE_ITERABLE, + ]; + + /** + * List of PHP builtin collection types. + * + * @var string[] + */ + public static $builtinCollectionTypes = [ + self::BUILTIN_TYPE_ARRAY, + self::BUILTIN_TYPE_ITERABLE, + ]; + + private string $builtinType; + private bool $nullable; + private ?string $class; + private bool $collection; + private array $collectionKeyType; + private array $collectionValueType; + + /** + * @param Type[]|Type|null $collectionKeyType + * @param Type[]|Type|null $collectionValueType + * + * @throws \InvalidArgumentException + */ + public function __construct(string $builtinType, bool $nullable = false, ?string $class = null, bool $collection = false, array|self|null $collectionKeyType = null, array|self|null $collectionValueType = null) + { + if (!\in_array($builtinType, self::$builtinTypes)) { + throw new \InvalidArgumentException(\sprintf('"%s" is not a valid PHP type.', $builtinType)); + } + + $this->builtinType = $builtinType; + $this->nullable = $nullable; + $this->class = $class; + $this->collection = $collection; + $this->collectionKeyType = $this->validateCollectionArgument($collectionKeyType, 5, '$collectionKeyType') ?? []; + $this->collectionValueType = $this->validateCollectionArgument($collectionValueType, 6, '$collectionValueType') ?? []; + } + + private function validateCollectionArgument(array|self|null $collectionArgument, int $argumentIndex, string $argumentName): ?array + { + if (null === $collectionArgument) { + return null; + } + + if (\is_array($collectionArgument)) { + foreach ($collectionArgument as $type) { + if (!$type instanceof self) { + throw new \TypeError(\sprintf('"%s()": Argument #%d (%s) must be of type "%s[]", "%s" or "null", array value "%s" given.', __METHOD__, $argumentIndex, $argumentName, self::class, self::class, get_debug_type($collectionArgument))); + } + } + + return $collectionArgument; + } + + return [$collectionArgument]; + } + + /** + * Gets built-in type. + * + * Can be bool, int, float, string, array, object, resource, null, callback or iterable. + */ + public function getBuiltinType(): string + { + return $this->builtinType; + } + + public function isNullable(): bool + { + return $this->nullable; + } + + /** + * Gets the class name. + * + * Only applicable if the built-in type is object. + */ + public function getClassName(): ?string + { + return $this->class; + } + + public function isCollection(): bool + { + return $this->collection; + } + + /** + * Gets collection key types. + * + * Only applicable for a collection type. + * + * @return Type[] + */ + public function getCollectionKeyTypes(): array + { + return $this->collectionKeyType; + } + + /** + * Gets collection value types. + * + * Only applicable for a collection type. + * + * @return Type[] + */ + public function getCollectionValueTypes(): array + { + return $this->collectionValueType; + } +} diff --git a/lib/symfony/property-info/Util/PhpDocTypeHelper.php b/lib/symfony/property-info/Util/PhpDocTypeHelper.php new file mode 100644 index 0000000000..686ea00136 --- /dev/null +++ b/lib/symfony/property-info/Util/PhpDocTypeHelper.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Util; + +use phpDocumentor\Reflection\PseudoType; +use phpDocumentor\Reflection\PseudoTypes\ConstExpression; +use phpDocumentor\Reflection\PseudoTypes\List_; +use phpDocumentor\Reflection\Type as DocType; +use phpDocumentor\Reflection\Types\Array_; +use phpDocumentor\Reflection\Types\Collection; +use phpDocumentor\Reflection\Types\Compound; +use phpDocumentor\Reflection\Types\Integer; +use phpDocumentor\Reflection\Types\Null_; +use phpDocumentor\Reflection\Types\Nullable; +use phpDocumentor\Reflection\Types\String_; +use Symfony\Component\PropertyInfo\Type; + +// Workaround for phpdocumentor/type-resolver < 1.6 +// We trigger the autoloader here, so we don't need to trigger it inside the loop later. +class_exists(List_::class); + +/** + * Transforms a php doc type to a {@link Type} instance. + * + * @author Kévin Dunglas + * @author Guilhem N. + */ +final class PhpDocTypeHelper +{ + /** + * Creates a {@see Type} from a PHPDoc type. + * + * @return Type[] + */ + public function getTypes(DocType $varType): array + { + if ($varType instanceof ConstExpression) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return []; + } + + $types = []; + $nullable = false; + + if ($varType instanceof Nullable) { + $nullable = true; + $varType = $varType->getActualType(); + } + + if (!$varType instanceof Compound) { + if ($varType instanceof Null_) { + $nullable = true; + } + + $type = $this->createType($varType, $nullable); + if (null !== $type) { + $types[] = $type; + } + + return $types; + } + + $varTypes = []; + for ($typeIndex = 0; $varType->has($typeIndex); ++$typeIndex) { + $type = $varType->get($typeIndex); + + if ($type instanceof ConstExpression) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return []; + } + + // If null is present, all types are nullable + if ($type instanceof Null_) { + $nullable = true; + continue; + } + + if ($type instanceof Nullable) { + $nullable = true; + $type = $type->getActualType(); + } + + $varTypes[] = $type; + } + + foreach ($varTypes as $varType) { + $type = $this->createType($varType, $nullable); + if (null !== $type) { + $types[] = $type; + } + } + + return $types; + } + + /** + * Creates a {@see Type} from a PHPDoc type. + */ + private function createType(DocType $type, bool $nullable): ?Type + { + $docType = (string) $type; + + if ($type instanceof Collection) { + $fqsen = $type->getFqsen(); + if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) { + // Workaround for phpdocumentor/type-resolver < 1.6 + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType())); + } + + [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); + + $collection = is_a($class, \Traversable::class, true) || is_a($class, \ArrayAccess::class, true); + + // it's safer to fall back to other extractors if the generic type is too abstract + if (!$collection && !class_exists($class)) { + return null; + } + + $keys = $this->getTypes($type->getKeyType()); + $values = $this->getTypes($type->getValueType()); + + return new Type($phpType, $nullable, $class, $collection, $keys, $values); + } + + // Cannot guess + if (!$docType || 'mixed' === $docType) { + return null; + } + + if (str_ends_with($docType, '[]') && $type instanceof Array_) { + $collectionKeyTypes = new Type(Type::BUILTIN_TYPE_INT); + $collectionValueTypes = $this->getTypes($type->getValueType()); + + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); + } + + if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) { + // array is converted to x[] which is handled above + // so it's only necessary to handle array here + $collectionKeyTypes = $this->getTypes($type->getKeyType()); + $collectionValueTypes = $this->getTypes($type->getValueType()); + + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); + } + + if ($type instanceof PseudoType) { + if ($type->underlyingType() instanceof Integer) { + return new Type(Type::BUILTIN_TYPE_INT, $nullable, null); + } elseif ($type->underlyingType() instanceof String_) { + return new Type(Type::BUILTIN_TYPE_STRING, $nullable, null); + } + } + + $docType = $this->normalizeType($docType); + [$phpType, $class] = $this->getPhpTypeAndClass($docType); + + if ('array' === $docType) { + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, null, null); + } + + return new Type($phpType, $nullable, $class); + } + + private function normalizeType(string $docType): string + { + return match ($docType) { + 'integer' => 'int', + 'boolean' => 'bool', + // real is not part of the PHPDoc standard, so we ignore it + 'double' => 'float', + 'callback' => 'callable', + 'void' => 'null', + default => $docType, + }; + } + + private function getPhpTypeAndClass(string $docType): array + { + if (\in_array($docType, Type::$builtinTypes)) { + return [$docType, null]; + } + + if (\in_array($docType, ['parent', 'self', 'static'], true)) { + return ['object', $docType]; + } + + return ['object', ltrim($docType, '\\')]; + } +} diff --git a/lib/symfony/property-info/Util/PhpStanTypeHelper.php b/lib/symfony/property-info/Util/PhpStanTypeHelper.php new file mode 100644 index 0000000000..c2395d6ca0 --- /dev/null +++ b/lib/symfony/property-info/Util/PhpStanTypeHelper.php @@ -0,0 +1,211 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Util; + +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use Symfony\Component\PropertyInfo\PhpStan\NameScope; +use Symfony\Component\PropertyInfo\Type; + +/** + * Transforms a php doc tag value to a {@link Type} instance. + * + * @author Baptiste Leduc + * + * @internal + */ +final class PhpStanTypeHelper +{ + /** + * Creates a {@see Type} from a PhpDocTagValueNode type. + * + * @return Type[] + */ + public function getTypes(PhpDocTagValueNode $node, NameScope $nameScope): array + { + if ($node instanceof ParamTagValueNode || $node instanceof ReturnTagValueNode || $node instanceof VarTagValueNode) { + return $this->compressNullableType($this->extractTypes($node->type, $nameScope)); + } + + return []; + } + + /** + * Because PhpStan extract null as a separated type when Symfony / PHP compress it in the first available type we + * need this method to mimic how Symfony want null types. + * + * @param Type[] $types + * + * @return Type[] + */ + private function compressNullableType(array $types): array + { + $firstTypeIndex = null; + $nullableTypeIndex = null; + + foreach ($types as $k => $type) { + if (null === $firstTypeIndex && Type::BUILTIN_TYPE_NULL !== $type->getBuiltinType() && !$type->isNullable()) { + $firstTypeIndex = $k; + } + + if (null === $nullableTypeIndex && Type::BUILTIN_TYPE_NULL === $type->getBuiltinType()) { + $nullableTypeIndex = $k; + } + + if (null !== $firstTypeIndex && null !== $nullableTypeIndex) { + break; + } + } + + if (null !== $firstTypeIndex && null !== $nullableTypeIndex) { + $firstType = $types[$firstTypeIndex]; + $types[$firstTypeIndex] = new Type( + $firstType->getBuiltinType(), + true, + $firstType->getClassName(), + $firstType->isCollection(), + $firstType->getCollectionKeyTypes(), + $firstType->getCollectionValueTypes() + ); + unset($types[$nullableTypeIndex]); + } + + return array_values($types); + } + + /** + * @return Type[] + */ + private function extractTypes(TypeNode $node, NameScope $nameScope): array + { + if ($node instanceof UnionTypeNode) { + $types = []; + foreach ($node->types as $type) { + if ($type instanceof ConstTypeNode) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return []; + } + foreach ($this->extractTypes($type, $nameScope) as $subType) { + $types[] = $subType; + } + } + + return $this->compressNullableType($types); + } + if ($node instanceof GenericTypeNode) { + if ('class-string' === $node->type->name) { + return [new Type(Type::BUILTIN_TYPE_STRING)]; + } + + [$mainType] = $this->extractTypes($node->type, $nameScope); + + if (Type::BUILTIN_TYPE_INT === $mainType->getBuiltinType()) { + return [$mainType]; + } + + $collection = $mainType->isCollection() || is_a($mainType->getClassName(), \Traversable::class, true) || is_a($mainType->getClassName(), \ArrayAccess::class, true); + + // it's safer to fall back to other extractors if the generic type is too abstract + if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) { + return []; + } + + $collectionKeyTypes = $mainType->getCollectionKeyTypes(); + $collectionKeyValues = []; + if (1 === \count($node->genericTypes)) { + foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $subType) { + $collectionKeyValues[] = $subType; + } + } elseif (2 === \count($node->genericTypes)) { + foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $keySubType) { + $collectionKeyTypes[] = $keySubType; + } + foreach ($this->extractTypes($node->genericTypes[1], $nameScope) as $valueSubType) { + $collectionKeyValues[] = $valueSubType; + } + } + + return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), $collection, $collectionKeyTypes, $collectionKeyValues)]; + } + if ($node instanceof ArrayShapeNode) { + return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]; + } + if ($node instanceof ArrayTypeNode) { + return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], $this->extractTypes($node->type, $nameScope))]; + } + if ($node instanceof CallableTypeNode || $node instanceof CallableTypeParameterNode) { + return [new Type(Type::BUILTIN_TYPE_CALLABLE)]; + } + if ($node instanceof NullableTypeNode) { + $subTypes = $this->extractTypes($node->type, $nameScope); + if (\count($subTypes) > 1) { + $subTypes[] = new Type(Type::BUILTIN_TYPE_NULL); + + return $subTypes; + } + + return [new Type($subTypes[0]->getBuiltinType(), true, $subTypes[0]->getClassName(), $subTypes[0]->isCollection(), $subTypes[0]->getCollectionKeyTypes(), $subTypes[0]->getCollectionValueTypes())]; + } + if ($node instanceof ThisTypeNode) { + return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveRootClass())]; + } + if ($node instanceof IdentifierTypeNode) { + if (\in_array($node->name, Type::$builtinTypes)) { + return [new Type($node->name, false, null, \in_array($node->name, Type::$builtinCollectionTypes))]; + } + + return match ($node->name) { + 'integer', + 'positive-int', + 'negative-int' => [new Type(Type::BUILTIN_TYPE_INT)], + 'double' => [new Type(Type::BUILTIN_TYPE_FLOAT)], + 'list', + 'non-empty-list' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))], + 'non-empty-array' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)], + 'mixed' => [], // mixed seems to be ignored in all other extractors + 'parent' => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $node->name)], + 'static', + 'self' => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveRootClass())], + 'class-string', + 'html-escaped-string', + 'lowercase-string', + 'non-empty-lowercase-string', + 'non-empty-string', + 'numeric-string', + 'trait-string', + 'interface-string', + 'literal-string' => [new Type(Type::BUILTIN_TYPE_STRING)], + 'void' => [new Type(Type::BUILTIN_TYPE_NULL)], + 'scalar' => [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT), new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_BOOL)], + 'number' => [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT)], + 'numeric' => [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT), new Type(Type::BUILTIN_TYPE_STRING)], + 'array-key' => [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], + default => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveStringName($node->name))], + }; + } + + return []; + } +} diff --git a/lib/symfony/property-info/composer.json b/lib/symfony/property-info/composer.json new file mode 100644 index 0000000000..495b51dc50 --- /dev/null +++ b/lib/symfony/property-info/composer.json @@ -0,0 +1,52 @@ +{ + "name": "symfony/property-info", + "type": "library", + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "keywords": [ + "property", + "type", + "phpdoc", + "symfony", + "validator", + "doctrine" + ], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "symfony/serializer": "^5.4|^6.4|^7.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/cache": "<5.4", + "symfony/serializer": "<5.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\PropertyInfo\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} From f0ff1c868e9d60303d5bebe1e59c889d22df6706 Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Thu, 9 Oct 2025 15:02:09 +0200 Subject: [PATCH 07/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20add=20symfony=20form=20test?= =?UTF-8?q?=20folder=20in=20ListDeniedFilesRelPaths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sources/Dependencies/Composer/iTopComposer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/Dependencies/Composer/iTopComposer.php b/sources/Dependencies/Composer/iTopComposer.php index d0512fbf0f..17deb0570d 100644 --- a/sources/Dependencies/Composer/iTopComposer.php +++ b/sources/Dependencies/Composer/iTopComposer.php @@ -84,6 +84,7 @@ public function ListDeniedFilesRelPaths(): array 'symfony/event-dispatcher/Tests', 'symfony/filesystem/Tests', 'symfony/finder/Tests', + 'symfony/form/Test', 'symfony/http-client-contracts/Test', 'symfony/http-foundation/Test', 'symfony/http-kernel/Tests', From 29f05fefe6f5d4f4ca70729400221fc7b1cc0303 Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Thu, 9 Oct 2025 15:26:29 +0200 Subject: [PATCH 08/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20add=20symfony=20form=20test?= =?UTF-8?q?=20folder=20in=20ListDeniedFilesRelPaths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/composer/installed.php | 4 +- .../form/Test/FormBuilderInterface.php | 18 ------ .../form/Test/FormIntegrationTestCase.php | 54 ---------------- lib/symfony/form/Test/FormInterface.php | 18 ------ .../form/Test/FormPerformanceTestCase.php | 64 ------------------- lib/symfony/form/Test/Traits/RunTestTrait.php | 36 ----------- .../Test/Traits/ValidatorExtensionTrait.php | 44 ------------- lib/symfony/form/Test/TypeTestCase.php | 58 ----------------- 8 files changed, 2 insertions(+), 294 deletions(-) delete mode 100644 lib/symfony/form/Test/FormBuilderInterface.php delete mode 100644 lib/symfony/form/Test/FormIntegrationTestCase.php delete mode 100644 lib/symfony/form/Test/FormInterface.php delete mode 100644 lib/symfony/form/Test/FormPerformanceTestCase.php delete mode 100644 lib/symfony/form/Test/Traits/RunTestTrait.php delete mode 100644 lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php delete mode 100644 lib/symfony/form/Test/TypeTestCase.php diff --git a/lib/composer/installed.php b/lib/composer/installed.php index eaaf931c8e..0a7711456a 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'c07c3cdca1e8472cb54809021b0856d8682455c6', + 'reference' => 'f0ff1c868e9d60303d5bebe1e59c889d22df6706', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'c07c3cdca1e8472cb54809021b0856d8682455c6', + 'reference' => 'f0ff1c868e9d60303d5bebe1e59c889d22df6706', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/lib/symfony/form/Test/FormBuilderInterface.php b/lib/symfony/form/Test/FormBuilderInterface.php deleted file mode 100644 index 185a8a12d6..0000000000 --- a/lib/symfony/form/Test/FormBuilderInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test; - -use Symfony\Component\Form\FormBuilderInterface as BaseFormBuilderInterface; - -interface FormBuilderInterface extends \Iterator, BaseFormBuilderInterface -{ -} diff --git a/lib/symfony/form/Test/FormIntegrationTestCase.php b/lib/symfony/form/Test/FormIntegrationTestCase.php deleted file mode 100644 index 5bf37fd48a..0000000000 --- a/lib/symfony/form/Test/FormIntegrationTestCase.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\Form\Forms; - -/** - * @author Bernhard Schussek - */ -abstract class FormIntegrationTestCase extends TestCase -{ - protected FormFactoryInterface $factory; - - protected function setUp(): void - { - $this->factory = Forms::createFormFactoryBuilder() - ->addExtensions($this->getExtensions()) - ->addTypeExtensions($this->getTypeExtensions()) - ->addTypes($this->getTypes()) - ->addTypeGuessers($this->getTypeGuessers()) - ->getFormFactory(); - } - - protected function getExtensions() - { - return []; - } - - protected function getTypeExtensions() - { - return []; - } - - protected function getTypes() - { - return []; - } - - protected function getTypeGuessers() - { - return []; - } -} diff --git a/lib/symfony/form/Test/FormInterface.php b/lib/symfony/form/Test/FormInterface.php deleted file mode 100644 index 4af4603087..0000000000 --- a/lib/symfony/form/Test/FormInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test; - -use Symfony\Component\Form\FormInterface as BaseFormInterface; - -interface FormInterface extends \Iterator, BaseFormInterface -{ -} diff --git a/lib/symfony/form/Test/FormPerformanceTestCase.php b/lib/symfony/form/Test/FormPerformanceTestCase.php deleted file mode 100644 index 2f7c307685..0000000000 --- a/lib/symfony/form/Test/FormPerformanceTestCase.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test; - -use Symfony\Component\Form\Test\Traits\RunTestTrait; - -/** - * Base class for performance tests. - * - * Copied from Doctrine 2's OrmPerformanceTestCase. - * - * @author robo - * @author Bernhard Schussek - */ -abstract class FormPerformanceTestCase extends FormIntegrationTestCase -{ - use RunTestTrait; - - /** - * @var int - */ - protected $maxRunningTime = 0; - - private function doRunTest(): mixed - { - $s = microtime(true); - $result = parent::runTest(); - $time = microtime(true) - $s; - - if (0 != $this->maxRunningTime && $time > $this->maxRunningTime) { - $this->fail(\sprintf('expected running time: <= %s but was: %s', $this->maxRunningTime, $time)); - } - - $this->expectNotToPerformAssertions(); - - return $result; - } - - /** - * @throws \InvalidArgumentException - */ - public function setMaxRunningTime(int $maxRunningTime) - { - if ($maxRunningTime < 0) { - throw new \InvalidArgumentException(); - } - - $this->maxRunningTime = $maxRunningTime; - } - - public function getMaxRunningTime(): int - { - return $this->maxRunningTime; - } -} diff --git a/lib/symfony/form/Test/Traits/RunTestTrait.php b/lib/symfony/form/Test/Traits/RunTestTrait.php deleted file mode 100644 index 17204b9670..0000000000 --- a/lib/symfony/form/Test/Traits/RunTestTrait.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test\Traits; - -use PHPUnit\Framework\TestCase; - -if ((new \ReflectionMethod(TestCase::class, 'runTest'))->hasReturnType()) { - // PHPUnit 10 - /** @internal */ - trait RunTestTrait - { - protected function runTest(): mixed - { - return $this->doRunTest(); - } - } -} else { - // PHPUnit 9 - /** @internal */ - trait RunTestTrait - { - protected function runTest() - { - return $this->doRunTest(); - } - } -} diff --git a/lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php b/lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php deleted file mode 100644 index 27c791e579..0000000000 --- a/lib/symfony/form/Test/Traits/ValidatorExtensionTrait.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test\Traits; - -use Symfony\Component\Form\Extension\Validator\ValidatorExtension; -use Symfony\Component\Form\Test\TypeTestCase; -use Symfony\Component\Validator\ConstraintViolationList; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -trait ValidatorExtensionTrait -{ - /** - * @var ValidatorInterface|null - */ - protected $validator; - - protected function getValidatorExtension(): ValidatorExtension - { - if (!interface_exists(ValidatorInterface::class)) { - throw new \Exception('In order to use the "ValidatorExtensionTrait", the symfony/validator component must be installed.'); - } - - if (!$this instanceof TypeTestCase) { - throw new \Exception(\sprintf('The trait "ValidatorExtensionTrait" can only be added to a class that extends "%s".', TypeTestCase::class)); - } - - $this->validator = $this->createMock(ValidatorInterface::class); - $metadata = $this->getMockBuilder(ClassMetadata::class)->setConstructorArgs([''])->onlyMethods(['addPropertyConstraint'])->getMock(); - $this->validator->expects($this->any())->method('getMetadataFor')->willReturn($metadata); - $this->validator->expects($this->any())->method('validate')->willReturn(new ConstraintViolationList()); - - return new ValidatorExtension($this->validator, false); - } -} diff --git a/lib/symfony/form/Test/TypeTestCase.php b/lib/symfony/form/Test/TypeTestCase.php deleted file mode 100644 index ac8eb9baa4..0000000000 --- a/lib/symfony/form/Test/TypeTestCase.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Test; - -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; - -abstract class TypeTestCase extends FormIntegrationTestCase -{ - /** - * @var FormBuilder - */ - protected $builder; - - /** - * @var EventDispatcherInterface - */ - protected $dispatcher; - - protected function setUp(): void - { - parent::setUp(); - - $this->dispatcher = $this->createMock(EventDispatcherInterface::class); - $this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory); - } - - protected function getExtensions() - { - $extensions = []; - - if (\in_array(ValidatorExtensionTrait::class, class_uses($this))) { - $extensions[] = $this->getValidatorExtension(); - } - - return $extensions; - } - - public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual) - { - self::assertEquals($expected->format('c'), $actual->format('c')); - } - - public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual) - { - self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS')); - } -} From 6183a9d3f3accabe858d865d1050861c1b0d57ef Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Fri, 10 Oct 2025 09:16:50 +0200 Subject: [PATCH 09/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20add=20Symfony=20csrf=20compo?= =?UTF-8?q?nent=20-=20organize=20controller=20-=20add=20twig=20debug=20ext?= =?UTF-8?q?ension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + composer.lock | 240 ++++++++++++++++- lib/composer/autoload_classmap.php | 133 +++++++++- lib/composer/autoload_psr4.php | 3 + lib/composer/autoload_static.php | 148 ++++++++++- lib/composer/installed.json | 247 ++++++++++++++++++ lib/composer/installed.php | 31 ++- lib/symfony/password-hasher/CHANGELOG.md | 13 + .../Command/UserPasswordHashCommand.php | 215 +++++++++++++++ .../Exception/ExceptionInterface.php | 21 ++ .../Exception/InvalidPasswordException.php | 23 ++ .../Exception/LogicException.php | 19 ++ .../Hasher/CheckPasswordLengthTrait.php | 25 ++ .../Hasher/MessageDigestPasswordHasher.php | 98 +++++++ .../Hasher/MigratingPasswordHasher.php | 64 +++++ .../Hasher/NativePasswordHasher.php | 117 +++++++++ .../Hasher/PasswordHasherAwareInterface.php | 26 ++ .../Hasher/PasswordHasherFactory.php | 240 +++++++++++++++++ .../Hasher/PasswordHasherFactoryInterface.php | 31 +++ .../Hasher/Pbkdf2PasswordHasher.php | 90 +++++++ .../Hasher/PlaintextPasswordHasher.php | 79 ++++++ .../Hasher/SodiumPasswordHasher.php | 114 ++++++++ .../Hasher/UserPasswordHasher.php | 71 +++++ .../Hasher/UserPasswordHasherInterface.php | 37 +++ lib/symfony/password-hasher/LICENSE | 19 ++ .../LegacyPasswordHasherInterface.php | 36 +++ .../PasswordHasherInterface.php | 43 +++ lib/symfony/password-hasher/README.md | 40 +++ lib/symfony/password-hasher/composer.json | 35 +++ .../AuthenticationTrustResolver.php | 38 +++ .../AuthenticationTrustResolverInterface.php | 38 +++ .../RememberMe/CacheTokenVerifier.php | 69 +++++ .../RememberMe/InMemoryTokenProvider.php | 72 +++++ .../RememberMe/PersistentToken.php | 73 ++++++ .../RememberMe/PersistentTokenInterface.php | 48 ++++ .../RememberMe/TokenProviderInterface.php | 56 ++++ .../RememberMe/TokenVerifierInterface.php | 32 +++ .../Authentication/Token/AbstractToken.php | 179 +++++++++++++ .../Authentication/Token/NullToken.php | 95 +++++++ .../Token/PreAuthenticatedToken.php | 56 ++++ .../Authentication/Token/RememberMeToken.php | 71 +++++ .../Token/Storage/TokenStorage.php | 70 +++++ .../Token/Storage/TokenStorageInterface.php | 36 +++ .../Storage/UsageTrackingTokenStorage.php | 83 ++++++ .../Authentication/Token/SwitchUserToken.php | 66 +++++ .../Authentication/Token/TokenInterface.php | 96 +++++++ .../Token/UsernamePasswordToken.php | 53 ++++ .../security-core/AuthenticationEvents.php | 34 +++ .../Authorization/AccessDecisionManager.php | 125 +++++++++ .../AccessDecisionManagerInterface.php | 30 +++ .../Authorization/AuthorizationChecker.php | 50 ++++ .../AuthorizationCheckerInterface.php | 27 ++ .../Authorization/ExpressionLanguage.php | 40 +++ .../ExpressionLanguageProvider.php | 36 +++ .../AccessDecisionStrategyInterface.php | 25 ++ .../Strategy/AffirmativeStrategy.php | 58 ++++ .../Strategy/ConsensusStrategy.php | 75 ++++++ .../Strategy/PriorityStrategy.php | 54 ++++ .../Strategy/UnanimousStrategy.php | 59 +++++ .../TraceableAccessDecisionManager.php | 109 ++++++++ .../Voter/AuthenticatedVoter.php | 104 ++++++++ .../Voter/CacheableVoterInterface.php | 30 +++ .../Authorization/Voter/ExpressionVoter.php | 99 +++++++ .../Voter/RoleHierarchyVoter.php | 41 +++ .../Authorization/Voter/RoleVoter.php | 68 +++++ .../Authorization/Voter/TraceableVoter.php | 59 +++++ .../Authorization/Voter/Voter.php | 95 +++++++ .../Authorization/Voter/VoterInterface.php | 41 +++ lib/symfony/security-core/CHANGELOG.md | 65 +++++ .../Event/AuthenticationEvent.php | 38 +++ .../Event/AuthenticationSuccessEvent.php | 16 ++ lib/symfony/security-core/Event/VoteEvent.php | 58 ++++ .../Exception/AccessDeniedException.php | 57 ++++ .../Exception/AccountExpiredException.php | 26 ++ .../Exception/AccountStatusException.php | 54 ++++ ...enticationCredentialsNotFoundException.php | 27 ++ .../Exception/AuthenticationException.php | 99 +++++++ .../AuthenticationExpiredException.php | 28 ++ .../AuthenticationServiceException.php | 26 ++ .../Exception/BadCredentialsException.php | 26 ++ .../Exception/CookieTheftException.php | 27 ++ .../Exception/CredentialsExpiredException.php | 26 ++ ...ustomUserMessageAccountStatusException.php | 70 +++++ ...stomUserMessageAuthenticationException.php | 70 +++++ .../Exception/DisabledException.php | 26 ++ .../Exception/ExceptionInterface.php | 21 ++ .../InsufficientAuthenticationException.php | 28 ++ .../Exception/InvalidArgumentException.php | 21 ++ .../Exception/InvalidCsrfTokenException.php | 26 ++ .../Exception/LazyResponseException.php | 34 +++ .../Exception/LockedException.php | 26 ++ .../Exception/LogicException.php | 21 ++ .../Exception/LogoutException.php | 25 ++ .../Exception/ProviderNotFoundException.php | 27 ++ .../Exception/RuntimeException.php | 21 ++ .../Exception/SessionUnavailableException.php | 32 +++ .../Exception/TokenNotFoundException.php | 26 ++ ...nyLoginAttemptsAuthenticationException.php | 53 ++++ .../Exception/UnsupportedUserException.php | 22 ++ .../Exception/UserNotFoundException.php | 61 +++++ lib/symfony/security-core/LICENSE | 19 ++ lib/symfony/security-core/README.md | 63 +++++ .../Resources/translations/security.af.xlf | 83 ++++++ .../Resources/translations/security.ar.xlf | 83 ++++++ .../Resources/translations/security.az.xlf | 83 ++++++ .../Resources/translations/security.be.xlf | 83 ++++++ .../Resources/translations/security.bg.xlf | 83 ++++++ .../Resources/translations/security.bs.xlf | 83 ++++++ .../Resources/translations/security.ca.xlf | 83 ++++++ .../Resources/translations/security.cs.xlf | 83 ++++++ .../Resources/translations/security.cy.xlf | 83 ++++++ .../Resources/translations/security.da.xlf | 83 ++++++ .../Resources/translations/security.de.xlf | 83 ++++++ .../Resources/translations/security.el.xlf | 83 ++++++ .../Resources/translations/security.en.xlf | 83 ++++++ .../Resources/translations/security.es.xlf | 83 ++++++ .../Resources/translations/security.et.xlf | 83 ++++++ .../Resources/translations/security.eu.xlf | 83 ++++++ .../Resources/translations/security.fa.xlf | 83 ++++++ .../Resources/translations/security.fi.xlf | 83 ++++++ .../Resources/translations/security.fr.xlf | 83 ++++++ .../Resources/translations/security.gl.xlf | 83 ++++++ .../Resources/translations/security.he.xlf | 83 ++++++ .../Resources/translations/security.hr.xlf | 83 ++++++ .../Resources/translations/security.hu.xlf | 83 ++++++ .../Resources/translations/security.hy.xlf | 83 ++++++ .../Resources/translations/security.id.xlf | 83 ++++++ .../Resources/translations/security.it.xlf | 83 ++++++ .../Resources/translations/security.ja.xlf | 83 ++++++ .../Resources/translations/security.lb.xlf | 83 ++++++ .../Resources/translations/security.lt.xlf | 83 ++++++ .../Resources/translations/security.lv.xlf | 83 ++++++ .../Resources/translations/security.mk.xlf | 83 ++++++ .../Resources/translations/security.mn.xlf | 83 ++++++ .../Resources/translations/security.my.xlf | 83 ++++++ .../Resources/translations/security.nb.xlf | 83 ++++++ .../Resources/translations/security.nl.xlf | 83 ++++++ .../Resources/translations/security.nn.xlf | 83 ++++++ .../Resources/translations/security.no.xlf | 83 ++++++ .../Resources/translations/security.pl.xlf | 83 ++++++ .../Resources/translations/security.pt.xlf | 83 ++++++ .../Resources/translations/security.pt_BR.xlf | 83 ++++++ .../Resources/translations/security.ro.xlf | 83 ++++++ .../Resources/translations/security.ru.xlf | 83 ++++++ .../Resources/translations/security.sk.xlf | 83 ++++++ .../Resources/translations/security.sl.xlf | 83 ++++++ .../Resources/translations/security.sq.xlf | 83 ++++++ .../translations/security.sr_Cyrl.xlf | 83 ++++++ .../translations/security.sr_Latn.xlf | 83 ++++++ .../Resources/translations/security.sv.xlf | 83 ++++++ .../Resources/translations/security.th.xlf | 83 ++++++ .../Resources/translations/security.tl.xlf | 83 ++++++ .../Resources/translations/security.tr.xlf | 83 ++++++ .../Resources/translations/security.uk.xlf | 83 ++++++ .../Resources/translations/security.ur.xlf | 83 ++++++ .../Resources/translations/security.uz.xlf | 83 ++++++ .../Resources/translations/security.vi.xlf | 83 ++++++ .../Resources/translations/security.zh_CN.xlf | 83 ++++++ .../Resources/translations/security.zh_TW.xlf | 83 ++++++ lib/symfony/security-core/Role/Role.php | 31 +++ .../security-core/Role/RoleHierarchy.php | 81 ++++++ .../Role/RoleHierarchyInterface.php | 27 ++ .../security-core/Role/SwitchUserRole.php | 23 ++ lib/symfony/security-core/Security.php | 69 +++++ .../Exception/ExpiredSignatureException.php | 21 ++ .../Exception/InvalidSignatureException.php | 21 ++ .../Signature/ExpiredSignatureStorage.php | 51 ++++ .../Signature/SignatureHasher.php | 135 ++++++++++ .../Test/AccessDecisionStrategyTestCase.php | 80 ++++++ .../AttributesBasedUserProviderInterface.php | 36 +++ .../security-core/User/ChainUserChecker.php | 36 +++ .../security-core/User/ChainUserProvider.php | 134 ++++++++++ .../security-core/User/EquatableInterface.php | 30 +++ .../security-core/User/InMemoryUser.php | 110 ++++++++ .../User/InMemoryUserChecker.php | 45 ++++ .../User/InMemoryUserProvider.php | 115 ++++++++ ...gacyPasswordAuthenticatedUserInterface.php | 28 ++ .../User/MissingUserProvider.php | 53 ++++ lib/symfony/security-core/User/OidcUser.php | 182 +++++++++++++ .../PasswordAuthenticatedUserInterface.php | 28 ++ .../User/PasswordUpgraderInterface.php | 27 ++ .../User/UserCheckerInterface.php | 43 +++ .../security-core/User/UserInterface.php | 61 +++++ .../User/UserProviderInterface.php | 70 +++++ .../Validator/Constraints/UserPassword.php | 44 ++++ .../Constraints/UserPasswordValidator.php | 69 +++++ lib/symfony/security-core/composer.json | 53 ++++ lib/symfony/security-csrf/CHANGELOG.md | 13 + lib/symfony/security-csrf/CsrfToken.php | 53 ++++ .../security-csrf/CsrfTokenManager.php | 141 ++++++++++ .../CsrfTokenManagerInterface.php | 57 ++++ .../Exception/TokenNotFoundException.php | 21 ++ lib/symfony/security-csrf/LICENSE | 19 ++ lib/symfony/security-csrf/README.md | 29 ++ .../TokenGeneratorInterface.php | 25 ++ .../TokenGenerator/UriSafeTokenGenerator.php | 46 ++++ .../ClearableTokenStorageInterface.php | 25 ++ .../NativeSessionTokenStorage.php | 112 ++++++++ .../TokenStorage/SessionTokenStorage.php | 112 ++++++++ .../TokenStorage/TokenStorageInterface.php | 47 ++++ lib/symfony/security-csrf/composer.json | 35 +++ .../TwigBase/Controller/Controller.php | 51 ++-- .../Application/TwigBase/Twig/Extension.php | 5 - .../Application/TwigBase/Twig/TwigHelper.php | 9 +- .../Extension/FormCompatibilityExtension.php | 47 ++++ ...out.twig => itop_console_layout.html.twig} | 0 206 files changed, 13254 insertions(+), 41 deletions(-) create mode 100644 lib/symfony/password-hasher/CHANGELOG.md create mode 100644 lib/symfony/password-hasher/Command/UserPasswordHashCommand.php create mode 100644 lib/symfony/password-hasher/Exception/ExceptionInterface.php create mode 100644 lib/symfony/password-hasher/Exception/InvalidPasswordException.php create mode 100644 lib/symfony/password-hasher/Exception/LogicException.php create mode 100644 lib/symfony/password-hasher/Hasher/CheckPasswordLengthTrait.php create mode 100644 lib/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/MigratingPasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/NativePasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/PasswordHasherAwareInterface.php create mode 100644 lib/symfony/password-hasher/Hasher/PasswordHasherFactory.php create mode 100644 lib/symfony/password-hasher/Hasher/PasswordHasherFactoryInterface.php create mode 100644 lib/symfony/password-hasher/Hasher/Pbkdf2PasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/PlaintextPasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/SodiumPasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/UserPasswordHasher.php create mode 100644 lib/symfony/password-hasher/Hasher/UserPasswordHasherInterface.php create mode 100644 lib/symfony/password-hasher/LICENSE create mode 100644 lib/symfony/password-hasher/LegacyPasswordHasherInterface.php create mode 100644 lib/symfony/password-hasher/PasswordHasherInterface.php create mode 100644 lib/symfony/password-hasher/README.md create mode 100644 lib/symfony/password-hasher/composer.json create mode 100644 lib/symfony/security-core/Authentication/AuthenticationTrustResolver.php create mode 100644 lib/symfony/security-core/Authentication/AuthenticationTrustResolverInterface.php create mode 100644 lib/symfony/security-core/Authentication/RememberMe/CacheTokenVerifier.php create mode 100644 lib/symfony/security-core/Authentication/RememberMe/InMemoryTokenProvider.php create mode 100644 lib/symfony/security-core/Authentication/RememberMe/PersistentToken.php create mode 100644 lib/symfony/security-core/Authentication/RememberMe/PersistentTokenInterface.php create mode 100644 lib/symfony/security-core/Authentication/RememberMe/TokenProviderInterface.php create mode 100644 lib/symfony/security-core/Authentication/RememberMe/TokenVerifierInterface.php create mode 100644 lib/symfony/security-core/Authentication/Token/AbstractToken.php create mode 100644 lib/symfony/security-core/Authentication/Token/NullToken.php create mode 100644 lib/symfony/security-core/Authentication/Token/PreAuthenticatedToken.php create mode 100644 lib/symfony/security-core/Authentication/Token/RememberMeToken.php create mode 100644 lib/symfony/security-core/Authentication/Token/Storage/TokenStorage.php create mode 100644 lib/symfony/security-core/Authentication/Token/Storage/TokenStorageInterface.php create mode 100644 lib/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php create mode 100644 lib/symfony/security-core/Authentication/Token/SwitchUserToken.php create mode 100644 lib/symfony/security-core/Authentication/Token/TokenInterface.php create mode 100644 lib/symfony/security-core/Authentication/Token/UsernamePasswordToken.php create mode 100644 lib/symfony/security-core/AuthenticationEvents.php create mode 100644 lib/symfony/security-core/Authorization/AccessDecisionManager.php create mode 100644 lib/symfony/security-core/Authorization/AccessDecisionManagerInterface.php create mode 100644 lib/symfony/security-core/Authorization/AuthorizationChecker.php create mode 100644 lib/symfony/security-core/Authorization/AuthorizationCheckerInterface.php create mode 100644 lib/symfony/security-core/Authorization/ExpressionLanguage.php create mode 100644 lib/symfony/security-core/Authorization/ExpressionLanguageProvider.php create mode 100644 lib/symfony/security-core/Authorization/Strategy/AccessDecisionStrategyInterface.php create mode 100644 lib/symfony/security-core/Authorization/Strategy/AffirmativeStrategy.php create mode 100644 lib/symfony/security-core/Authorization/Strategy/ConsensusStrategy.php create mode 100644 lib/symfony/security-core/Authorization/Strategy/PriorityStrategy.php create mode 100644 lib/symfony/security-core/Authorization/Strategy/UnanimousStrategy.php create mode 100644 lib/symfony/security-core/Authorization/TraceableAccessDecisionManager.php create mode 100644 lib/symfony/security-core/Authorization/Voter/AuthenticatedVoter.php create mode 100644 lib/symfony/security-core/Authorization/Voter/CacheableVoterInterface.php create mode 100644 lib/symfony/security-core/Authorization/Voter/ExpressionVoter.php create mode 100644 lib/symfony/security-core/Authorization/Voter/RoleHierarchyVoter.php create mode 100644 lib/symfony/security-core/Authorization/Voter/RoleVoter.php create mode 100644 lib/symfony/security-core/Authorization/Voter/TraceableVoter.php create mode 100644 lib/symfony/security-core/Authorization/Voter/Voter.php create mode 100644 lib/symfony/security-core/Authorization/Voter/VoterInterface.php create mode 100644 lib/symfony/security-core/CHANGELOG.md create mode 100644 lib/symfony/security-core/Event/AuthenticationEvent.php create mode 100644 lib/symfony/security-core/Event/AuthenticationSuccessEvent.php create mode 100644 lib/symfony/security-core/Event/VoteEvent.php create mode 100644 lib/symfony/security-core/Exception/AccessDeniedException.php create mode 100644 lib/symfony/security-core/Exception/AccountExpiredException.php create mode 100644 lib/symfony/security-core/Exception/AccountStatusException.php create mode 100644 lib/symfony/security-core/Exception/AuthenticationCredentialsNotFoundException.php create mode 100644 lib/symfony/security-core/Exception/AuthenticationException.php create mode 100644 lib/symfony/security-core/Exception/AuthenticationExpiredException.php create mode 100644 lib/symfony/security-core/Exception/AuthenticationServiceException.php create mode 100644 lib/symfony/security-core/Exception/BadCredentialsException.php create mode 100644 lib/symfony/security-core/Exception/CookieTheftException.php create mode 100644 lib/symfony/security-core/Exception/CredentialsExpiredException.php create mode 100644 lib/symfony/security-core/Exception/CustomUserMessageAccountStatusException.php create mode 100644 lib/symfony/security-core/Exception/CustomUserMessageAuthenticationException.php create mode 100644 lib/symfony/security-core/Exception/DisabledException.php create mode 100644 lib/symfony/security-core/Exception/ExceptionInterface.php create mode 100644 lib/symfony/security-core/Exception/InsufficientAuthenticationException.php create mode 100644 lib/symfony/security-core/Exception/InvalidArgumentException.php create mode 100644 lib/symfony/security-core/Exception/InvalidCsrfTokenException.php create mode 100644 lib/symfony/security-core/Exception/LazyResponseException.php create mode 100644 lib/symfony/security-core/Exception/LockedException.php create mode 100644 lib/symfony/security-core/Exception/LogicException.php create mode 100644 lib/symfony/security-core/Exception/LogoutException.php create mode 100644 lib/symfony/security-core/Exception/ProviderNotFoundException.php create mode 100644 lib/symfony/security-core/Exception/RuntimeException.php create mode 100644 lib/symfony/security-core/Exception/SessionUnavailableException.php create mode 100644 lib/symfony/security-core/Exception/TokenNotFoundException.php create mode 100644 lib/symfony/security-core/Exception/TooManyLoginAttemptsAuthenticationException.php create mode 100644 lib/symfony/security-core/Exception/UnsupportedUserException.php create mode 100644 lib/symfony/security-core/Exception/UserNotFoundException.php create mode 100644 lib/symfony/security-core/LICENSE create mode 100644 lib/symfony/security-core/README.md create mode 100644 lib/symfony/security-core/Resources/translations/security.af.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.ar.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.az.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.be.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.bg.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.bs.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.ca.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.cs.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.cy.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.da.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.de.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.el.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.en.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.es.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.et.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.eu.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.fa.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.fi.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.fr.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.gl.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.he.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.hr.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.hu.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.hy.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.id.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.it.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.ja.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.lb.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.lt.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.lv.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.mk.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.mn.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.my.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.nb.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.nl.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.nn.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.no.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.pl.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.pt.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.pt_BR.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.ro.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.ru.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.sk.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.sl.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.sq.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.sr_Cyrl.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.sr_Latn.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.sv.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.th.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.tl.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.tr.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.uk.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.ur.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.uz.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.vi.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.zh_CN.xlf create mode 100644 lib/symfony/security-core/Resources/translations/security.zh_TW.xlf create mode 100644 lib/symfony/security-core/Role/Role.php create mode 100644 lib/symfony/security-core/Role/RoleHierarchy.php create mode 100644 lib/symfony/security-core/Role/RoleHierarchyInterface.php create mode 100644 lib/symfony/security-core/Role/SwitchUserRole.php create mode 100644 lib/symfony/security-core/Security.php create mode 100644 lib/symfony/security-core/Signature/Exception/ExpiredSignatureException.php create mode 100644 lib/symfony/security-core/Signature/Exception/InvalidSignatureException.php create mode 100644 lib/symfony/security-core/Signature/ExpiredSignatureStorage.php create mode 100644 lib/symfony/security-core/Signature/SignatureHasher.php create mode 100644 lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php create mode 100644 lib/symfony/security-core/User/AttributesBasedUserProviderInterface.php create mode 100644 lib/symfony/security-core/User/ChainUserChecker.php create mode 100644 lib/symfony/security-core/User/ChainUserProvider.php create mode 100644 lib/symfony/security-core/User/EquatableInterface.php create mode 100644 lib/symfony/security-core/User/InMemoryUser.php create mode 100644 lib/symfony/security-core/User/InMemoryUserChecker.php create mode 100644 lib/symfony/security-core/User/InMemoryUserProvider.php create mode 100644 lib/symfony/security-core/User/LegacyPasswordAuthenticatedUserInterface.php create mode 100644 lib/symfony/security-core/User/MissingUserProvider.php create mode 100644 lib/symfony/security-core/User/OidcUser.php create mode 100644 lib/symfony/security-core/User/PasswordAuthenticatedUserInterface.php create mode 100644 lib/symfony/security-core/User/PasswordUpgraderInterface.php create mode 100644 lib/symfony/security-core/User/UserCheckerInterface.php create mode 100644 lib/symfony/security-core/User/UserInterface.php create mode 100644 lib/symfony/security-core/User/UserProviderInterface.php create mode 100644 lib/symfony/security-core/Validator/Constraints/UserPassword.php create mode 100644 lib/symfony/security-core/Validator/Constraints/UserPasswordValidator.php create mode 100644 lib/symfony/security-core/composer.json create mode 100644 lib/symfony/security-csrf/CHANGELOG.md create mode 100644 lib/symfony/security-csrf/CsrfToken.php create mode 100644 lib/symfony/security-csrf/CsrfTokenManager.php create mode 100644 lib/symfony/security-csrf/CsrfTokenManagerInterface.php create mode 100644 lib/symfony/security-csrf/Exception/TokenNotFoundException.php create mode 100644 lib/symfony/security-csrf/LICENSE create mode 100644 lib/symfony/security-csrf/README.md create mode 100644 lib/symfony/security-csrf/TokenGenerator/TokenGeneratorInterface.php create mode 100644 lib/symfony/security-csrf/TokenGenerator/UriSafeTokenGenerator.php create mode 100644 lib/symfony/security-csrf/TokenStorage/ClearableTokenStorageInterface.php create mode 100644 lib/symfony/security-csrf/TokenStorage/NativeSessionTokenStorage.php create mode 100644 lib/symfony/security-csrf/TokenStorage/SessionTokenStorage.php create mode 100644 lib/symfony/security-csrf/TokenStorage/TokenStorageInterface.php create mode 100644 lib/symfony/security-csrf/composer.json create mode 100644 sources/Forms/Twig/Extension/FormCompatibilityExtension.php rename templates/application/forms/{itop_console_layout.twig => itop_console_layout.html.twig} (100%) diff --git a/composer.json b/composer.json index 1e83a16e23..36f3e3ffc7 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "symfony/http-kernel": "~6.4.0", "symfony/mailer": "^6.4", "symfony/runtime": "~6.4.0", + "symfony/security-csrf": "^6.4", "symfony/twig-bundle": "~6.4.0", "symfony/var-dumper": "~6.4.0", "symfony/yaml": "~6.4.0", diff --git a/composer.lock b/composer.lock index d7d3107866..e11f656f72 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": "104c5ecdb6e4797332831f4e7ba3e3ae", + "content-hash": "a7cd994da3f14c87f47faf5d985b86fa", "packages": [ { "name": "apereo/phpcas", @@ -3445,6 +3445,82 @@ ], "time": "2025-08-04T17:06:28+00:00" }, + { + "name": "symfony/password-hasher", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/dcab5ac87450aaed26483ba49c2ce86808da7557", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -4376,6 +4452,168 @@ ], "time": "2025-07-10T08:14:14+00:00" }, + { + "name": "symfony/security-core", + "version": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0", + "reference": "8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-02T19:15:26+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "9a1efc8c10b86bcedc9233affd10c716b54ca1b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/9a1efc8c10b86bcedc9233affd10c716b54ca1b7", + "reference": "9a1efc8c10b86bcedc9233affd10c716b54ca1b7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-foundation": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index d4267c33c8..76dffb3c0a 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -471,6 +471,7 @@ 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => $baseDir . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => $baseDir . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => $baseDir . '/sources/Form/Validator/SelectObjectValidator.php', + 'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => $baseDir . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php', 'Combodo\\iTop\\Kernel' => $baseDir . '/sources/Kernel.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php', @@ -2417,13 +2418,6 @@ 'Symfony\\Component\\Form\\SubmitButton' => $vendorDir . '/symfony/form/SubmitButton.php', 'Symfony\\Component\\Form\\SubmitButtonBuilder' => $vendorDir . '/symfony/form/SubmitButtonBuilder.php', 'Symfony\\Component\\Form\\SubmitButtonTypeInterface' => $vendorDir . '/symfony/form/SubmitButtonTypeInterface.php', - 'Symfony\\Component\\Form\\Test\\FormBuilderInterface' => $vendorDir . '/symfony/form/Test/FormBuilderInterface.php', - 'Symfony\\Component\\Form\\Test\\FormIntegrationTestCase' => $vendorDir . '/symfony/form/Test/FormIntegrationTestCase.php', - 'Symfony\\Component\\Form\\Test\\FormInterface' => $vendorDir . '/symfony/form/Test/FormInterface.php', - 'Symfony\\Component\\Form\\Test\\FormPerformanceTestCase' => $vendorDir . '/symfony/form/Test/FormPerformanceTestCase.php', - 'Symfony\\Component\\Form\\Test\\Traits\\RunTestTrait' => $vendorDir . '/symfony/form/Test/Traits/RunTestTrait.php', - 'Symfony\\Component\\Form\\Test\\Traits\\ValidatorExtensionTrait' => $vendorDir . '/symfony/form/Test/Traits/ValidatorExtensionTrait.php', - 'Symfony\\Component\\Form\\Test\\TypeTestCase' => $vendorDir . '/symfony/form/Test/TypeTestCase.php', 'Symfony\\Component\\Form\\Util\\FormUtil' => $vendorDir . '/symfony/form/Util/FormUtil.php', 'Symfony\\Component\\Form\\Util\\InheritDataAwareIterator' => $vendorDir . '/symfony/form/Util/InheritDataAwareIterator.php', 'Symfony\\Component\\Form\\Util\\OptionsResolverWrapper' => $vendorDir . '/symfony/form/Util/OptionsResolverWrapper.php', @@ -2824,6 +2818,24 @@ 'Symfony\\Component\\OptionsResolver\\OptionConfigurator' => $vendorDir . '/symfony/options-resolver/OptionConfigurator.php', 'Symfony\\Component\\OptionsResolver\\Options' => $vendorDir . '/symfony/options-resolver/Options.php', 'Symfony\\Component\\OptionsResolver\\OptionsResolver' => $vendorDir . '/symfony/options-resolver/OptionsResolver.php', + 'Symfony\\Component\\PasswordHasher\\Command\\UserPasswordHashCommand' => $vendorDir . '/symfony/password-hasher/Command/UserPasswordHashCommand.php', + 'Symfony\\Component\\PasswordHasher\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/password-hasher/Exception/ExceptionInterface.php', + 'Symfony\\Component\\PasswordHasher\\Exception\\InvalidPasswordException' => $vendorDir . '/symfony/password-hasher/Exception/InvalidPasswordException.php', + 'Symfony\\Component\\PasswordHasher\\Exception\\LogicException' => $vendorDir . '/symfony/password-hasher/Exception/LogicException.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\CheckPasswordLengthTrait' => $vendorDir . '/symfony/password-hasher/Hasher/CheckPasswordLengthTrait.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\MessageDigestPasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\MigratingPasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/MigratingPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\NativePasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/NativePasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherAwareInterface' => $vendorDir . '/symfony/password-hasher/Hasher/PasswordHasherAwareInterface.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactory' => $vendorDir . '/symfony/password-hasher/Hasher/PasswordHasherFactory.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactoryInterface' => $vendorDir . '/symfony/password-hasher/Hasher/PasswordHasherFactoryInterface.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\Pbkdf2PasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/Pbkdf2PasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PlaintextPasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/PlaintextPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\SodiumPasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/SodiumPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\UserPasswordHasher' => $vendorDir . '/symfony/password-hasher/Hasher/UserPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\UserPasswordHasherInterface' => $vendorDir . '/symfony/password-hasher/Hasher/UserPasswordHasherInterface.php', + 'Symfony\\Component\\PasswordHasher\\LegacyPasswordHasherInterface' => $vendorDir . '/symfony/password-hasher/LegacyPasswordHasherInterface.php', + 'Symfony\\Component\\PasswordHasher\\PasswordHasherInterface' => $vendorDir . '/symfony/password-hasher/PasswordHasherInterface.php', 'Symfony\\Component\\PropertyAccess\\Exception\\AccessException' => $vendorDir . '/symfony/property-access/Exception/AccessException.php', 'Symfony\\Component\\PropertyAccess\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/property-access/Exception/ExceptionInterface.php', 'Symfony\\Component\\PropertyAccess\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/property-access/Exception/InvalidArgumentException.php', @@ -2954,6 +2966,113 @@ 'Symfony\\Component\\Runtime\\Runner\\Symfony\\ResponseRunner' => $vendorDir . '/symfony/runtime/Runner/Symfony/ResponseRunner.php', 'Symfony\\Component\\Runtime\\RuntimeInterface' => $vendorDir . '/symfony/runtime/RuntimeInterface.php', 'Symfony\\Component\\Runtime\\SymfonyRuntime' => $vendorDir . '/symfony/runtime/SymfonyRuntime.php', + 'Symfony\\Component\\Security\\Core\\AuthenticationEvents' => $vendorDir . '/symfony/security-core/AuthenticationEvents.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationTrustResolver' => $vendorDir . '/symfony/security-core/Authentication/AuthenticationTrustResolver.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationTrustResolverInterface' => $vendorDir . '/symfony/security-core/Authentication/AuthenticationTrustResolverInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\CacheTokenVerifier' => $vendorDir . '/symfony/security-core/Authentication/RememberMe/CacheTokenVerifier.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\InMemoryTokenProvider' => $vendorDir . '/symfony/security-core/Authentication/RememberMe/InMemoryTokenProvider.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\PersistentToken' => $vendorDir . '/symfony/security-core/Authentication/RememberMe/PersistentToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\PersistentTokenInterface' => $vendorDir . '/symfony/security-core/Authentication/RememberMe/PersistentTokenInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\TokenProviderInterface' => $vendorDir . '/symfony/security-core/Authentication/RememberMe/TokenProviderInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\TokenVerifierInterface' => $vendorDir . '/symfony/security-core/Authentication/RememberMe/TokenVerifierInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken' => $vendorDir . '/symfony/security-core/Authentication/Token/AbstractToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\NullToken' => $vendorDir . '/symfony/security-core/Authentication/Token/NullToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\PreAuthenticatedToken' => $vendorDir . '/symfony/security-core/Authentication/Token/PreAuthenticatedToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\RememberMeToken' => $vendorDir . '/symfony/security-core/Authentication/Token/RememberMeToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorage' => $vendorDir . '/symfony/security-core/Authentication/Token/Storage/TokenStorage.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface' => $vendorDir . '/symfony/security-core/Authentication/Token/Storage/TokenStorageInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\UsageTrackingTokenStorage' => $vendorDir . '/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\SwitchUserToken' => $vendorDir . '/symfony/security-core/Authentication/Token/SwitchUserToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface' => $vendorDir . '/symfony/security-core/Authentication/Token/TokenInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken' => $vendorDir . '/symfony/security-core/Authentication/Token/UsernamePasswordToken.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager' => $vendorDir . '/symfony/security-core/Authorization/AccessDecisionManager.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface' => $vendorDir . '/symfony/security-core/Authorization/AccessDecisionManagerInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker' => $vendorDir . '/symfony/security-core/Authorization/AuthorizationChecker.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface' => $vendorDir . '/symfony/security-core/Authorization/AuthorizationCheckerInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\ExpressionLanguage' => $vendorDir . '/symfony/security-core/Authorization/ExpressionLanguage.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\ExpressionLanguageProvider' => $vendorDir . '/symfony/security-core/Authorization/ExpressionLanguageProvider.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\AccessDecisionStrategyInterface' => $vendorDir . '/symfony/security-core/Authorization/Strategy/AccessDecisionStrategyInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\AffirmativeStrategy' => $vendorDir . '/symfony/security-core/Authorization/Strategy/AffirmativeStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\ConsensusStrategy' => $vendorDir . '/symfony/security-core/Authorization/Strategy/ConsensusStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\PriorityStrategy' => $vendorDir . '/symfony/security-core/Authorization/Strategy/PriorityStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\UnanimousStrategy' => $vendorDir . '/symfony/security-core/Authorization/Strategy/UnanimousStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\TraceableAccessDecisionManager' => $vendorDir . '/symfony/security-core/Authorization/TraceableAccessDecisionManager.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AuthenticatedVoter' => $vendorDir . '/symfony/security-core/Authorization/Voter/AuthenticatedVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface' => $vendorDir . '/symfony/security-core/Authorization/Voter/CacheableVoterInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter' => $vendorDir . '/symfony/security-core/Authorization/Voter/ExpressionVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleHierarchyVoter' => $vendorDir . '/symfony/security-core/Authorization/Voter/RoleHierarchyVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter' => $vendorDir . '/symfony/security-core/Authorization/Voter/RoleVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\TraceableVoter' => $vendorDir . '/symfony/security-core/Authorization/Voter/TraceableVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter' => $vendorDir . '/symfony/security-core/Authorization/Voter/Voter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface' => $vendorDir . '/symfony/security-core/Authorization/Voter/VoterInterface.php', + 'Symfony\\Component\\Security\\Core\\Event\\AuthenticationEvent' => $vendorDir . '/symfony/security-core/Event/AuthenticationEvent.php', + 'Symfony\\Component\\Security\\Core\\Event\\AuthenticationSuccessEvent' => $vendorDir . '/symfony/security-core/Event/AuthenticationSuccessEvent.php', + 'Symfony\\Component\\Security\\Core\\Event\\VoteEvent' => $vendorDir . '/symfony/security-core/Event/VoteEvent.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException' => $vendorDir . '/symfony/security-core/Exception/AccessDeniedException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AccountExpiredException' => $vendorDir . '/symfony/security-core/Exception/AccountExpiredException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AccountStatusException' => $vendorDir . '/symfony/security-core/Exception/AccountStatusException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationCredentialsNotFoundException' => $vendorDir . '/symfony/security-core/Exception/AuthenticationCredentialsNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException' => $vendorDir . '/symfony/security-core/Exception/AuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException' => $vendorDir . '/symfony/security-core/Exception/AuthenticationExpiredException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationServiceException' => $vendorDir . '/symfony/security-core/Exception/AuthenticationServiceException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException' => $vendorDir . '/symfony/security-core/Exception/BadCredentialsException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CookieTheftException' => $vendorDir . '/symfony/security-core/Exception/CookieTheftException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CredentialsExpiredException' => $vendorDir . '/symfony/security-core/Exception/CredentialsExpiredException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAccountStatusException' => $vendorDir . '/symfony/security-core/Exception/CustomUserMessageAccountStatusException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException' => $vendorDir . '/symfony/security-core/Exception/CustomUserMessageAuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\DisabledException' => $vendorDir . '/symfony/security-core/Exception/DisabledException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/security-core/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Security\\Core\\Exception\\InsufficientAuthenticationException' => $vendorDir . '/symfony/security-core/Exception/InsufficientAuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/security-core/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\InvalidCsrfTokenException' => $vendorDir . '/symfony/security-core/Exception/InvalidCsrfTokenException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LazyResponseException' => $vendorDir . '/symfony/security-core/Exception/LazyResponseException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LockedException' => $vendorDir . '/symfony/security-core/Exception/LockedException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LogicException' => $vendorDir . '/symfony/security-core/Exception/LogicException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LogoutException' => $vendorDir . '/symfony/security-core/Exception/LogoutException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\ProviderNotFoundException' => $vendorDir . '/symfony/security-core/Exception/ProviderNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\RuntimeException' => $vendorDir . '/symfony/security-core/Exception/RuntimeException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\SessionUnavailableException' => $vendorDir . '/symfony/security-core/Exception/SessionUnavailableException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\TokenNotFoundException' => $vendorDir . '/symfony/security-core/Exception/TokenNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\TooManyLoginAttemptsAuthenticationException' => $vendorDir . '/symfony/security-core/Exception/TooManyLoginAttemptsAuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\UnsupportedUserException' => $vendorDir . '/symfony/security-core/Exception/UnsupportedUserException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\UserNotFoundException' => $vendorDir . '/symfony/security-core/Exception/UserNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Role\\Role' => $vendorDir . '/symfony/security-core/Role/Role.php', + 'Symfony\\Component\\Security\\Core\\Role\\RoleHierarchy' => $vendorDir . '/symfony/security-core/Role/RoleHierarchy.php', + 'Symfony\\Component\\Security\\Core\\Role\\RoleHierarchyInterface' => $vendorDir . '/symfony/security-core/Role/RoleHierarchyInterface.php', + 'Symfony\\Component\\Security\\Core\\Role\\SwitchUserRole' => $vendorDir . '/symfony/security-core/Role/SwitchUserRole.php', + 'Symfony\\Component\\Security\\Core\\Security' => $vendorDir . '/symfony/security-core/Security.php', + 'Symfony\\Component\\Security\\Core\\Signature\\Exception\\ExpiredSignatureException' => $vendorDir . '/symfony/security-core/Signature/Exception/ExpiredSignatureException.php', + 'Symfony\\Component\\Security\\Core\\Signature\\Exception\\InvalidSignatureException' => $vendorDir . '/symfony/security-core/Signature/Exception/InvalidSignatureException.php', + 'Symfony\\Component\\Security\\Core\\Signature\\ExpiredSignatureStorage' => $vendorDir . '/symfony/security-core/Signature/ExpiredSignatureStorage.php', + 'Symfony\\Component\\Security\\Core\\Signature\\SignatureHasher' => $vendorDir . '/symfony/security-core/Signature/SignatureHasher.php', + 'Symfony\\Component\\Security\\Core\\Test\\AccessDecisionStrategyTestCase' => $vendorDir . '/symfony/security-core/Test/AccessDecisionStrategyTestCase.php', + 'Symfony\\Component\\Security\\Core\\User\\AttributesBasedUserProviderInterface' => $vendorDir . '/symfony/security-core/User/AttributesBasedUserProviderInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\ChainUserChecker' => $vendorDir . '/symfony/security-core/User/ChainUserChecker.php', + 'Symfony\\Component\\Security\\Core\\User\\ChainUserProvider' => $vendorDir . '/symfony/security-core/User/ChainUserProvider.php', + 'Symfony\\Component\\Security\\Core\\User\\EquatableInterface' => $vendorDir . '/symfony/security-core/User/EquatableInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\InMemoryUser' => $vendorDir . '/symfony/security-core/User/InMemoryUser.php', + 'Symfony\\Component\\Security\\Core\\User\\InMemoryUserChecker' => $vendorDir . '/symfony/security-core/User/InMemoryUserChecker.php', + 'Symfony\\Component\\Security\\Core\\User\\InMemoryUserProvider' => $vendorDir . '/symfony/security-core/User/InMemoryUserProvider.php', + 'Symfony\\Component\\Security\\Core\\User\\LegacyPasswordAuthenticatedUserInterface' => $vendorDir . '/symfony/security-core/User/LegacyPasswordAuthenticatedUserInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\MissingUserProvider' => $vendorDir . '/symfony/security-core/User/MissingUserProvider.php', + 'Symfony\\Component\\Security\\Core\\User\\OidcUser' => $vendorDir . '/symfony/security-core/User/OidcUser.php', + 'Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface' => $vendorDir . '/symfony/security-core/User/PasswordAuthenticatedUserInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface' => $vendorDir . '/symfony/security-core/User/PasswordUpgraderInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\UserCheckerInterface' => $vendorDir . '/symfony/security-core/User/UserCheckerInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\UserInterface' => $vendorDir . '/symfony/security-core/User/UserInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\UserProviderInterface' => $vendorDir . '/symfony/security-core/User/UserProviderInterface.php', + 'Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPassword' => $vendorDir . '/symfony/security-core/Validator/Constraints/UserPassword.php', + 'Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPasswordValidator' => $vendorDir . '/symfony/security-core/Validator/Constraints/UserPasswordValidator.php', + 'Symfony\\Component\\Security\\Csrf\\CsrfToken' => $vendorDir . '/symfony/security-csrf/CsrfToken.php', + 'Symfony\\Component\\Security\\Csrf\\CsrfTokenManager' => $vendorDir . '/symfony/security-csrf/CsrfTokenManager.php', + 'Symfony\\Component\\Security\\Csrf\\CsrfTokenManagerInterface' => $vendorDir . '/symfony/security-csrf/CsrfTokenManagerInterface.php', + 'Symfony\\Component\\Security\\Csrf\\Exception\\TokenNotFoundException' => $vendorDir . '/symfony/security-csrf/Exception/TokenNotFoundException.php', + 'Symfony\\Component\\Security\\Csrf\\TokenGenerator\\TokenGeneratorInterface' => $vendorDir . '/symfony/security-csrf/TokenGenerator/TokenGeneratorInterface.php', + 'Symfony\\Component\\Security\\Csrf\\TokenGenerator\\UriSafeTokenGenerator' => $vendorDir . '/symfony/security-csrf/TokenGenerator/UriSafeTokenGenerator.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\ClearableTokenStorageInterface' => $vendorDir . '/symfony/security-csrf/TokenStorage/ClearableTokenStorageInterface.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\NativeSessionTokenStorage' => $vendorDir . '/symfony/security-csrf/TokenStorage/NativeSessionTokenStorage.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\SessionTokenStorage' => $vendorDir . '/symfony/security-csrf/TokenStorage/SessionTokenStorage.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\TokenStorageInterface' => $vendorDir . '/symfony/security-csrf/TokenStorage/TokenStorageInterface.php', 'Symfony\\Component\\Stopwatch\\Section' => $vendorDir . '/symfony/stopwatch/Section.php', 'Symfony\\Component\\Stopwatch\\Stopwatch' => $vendorDir . '/symfony/stopwatch/Stopwatch.php', 'Symfony\\Component\\Stopwatch\\StopwatchEvent' => $vendorDir . '/symfony/stopwatch/StopwatchEvent.php', diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 83a2f05fe6..f9549b07de 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -25,10 +25,13 @@ 'Symfony\\Component\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'), 'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'), 'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'), + 'Symfony\\Component\\Security\\Csrf\\' => array($vendorDir . '/symfony/security-csrf'), + 'Symfony\\Component\\Security\\Core\\' => array($vendorDir . '/symfony/security-core'), 'Symfony\\Component\\Runtime\\' => array($vendorDir . '/symfony/runtime'), 'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'), 'Symfony\\Component\\PropertyInfo\\' => array($vendorDir . '/symfony/property-info'), 'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'), + 'Symfony\\Component\\PasswordHasher\\' => array($vendorDir . '/symfony/password-hasher'), 'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'), 'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'), 'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'), diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 30adedc4c7..9477d8762c 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -52,10 +52,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\VarDumper\\' => 28, 'Symfony\\Component\\String\\' => 25, 'Symfony\\Component\\Stopwatch\\' => 28, + 'Symfony\\Component\\Security\\Csrf\\' => 32, + 'Symfony\\Component\\Security\\Core\\' => 32, 'Symfony\\Component\\Runtime\\' => 26, 'Symfony\\Component\\Routing\\' => 26, 'Symfony\\Component\\PropertyInfo\\' => 31, 'Symfony\\Component\\PropertyAccess\\' => 33, + 'Symfony\\Component\\PasswordHasher\\' => 33, 'Symfony\\Component\\OptionsResolver\\' => 34, 'Symfony\\Component\\Mime\\' => 23, 'Symfony\\Component\\Mailer\\' => 25, @@ -193,6 +196,14 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f array ( 0 => __DIR__ . '/..' . '/symfony/stopwatch', ), + 'Symfony\\Component\\Security\\Csrf\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/security-csrf', + ), + 'Symfony\\Component\\Security\\Core\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/security-core', + ), 'Symfony\\Component\\Runtime\\' => array ( 0 => __DIR__ . '/..' . '/symfony/runtime', @@ -209,6 +220,10 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f array ( 0 => __DIR__ . '/..' . '/symfony/property-access', ), + 'Symfony\\Component\\PasswordHasher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/password-hasher', + ), 'Symfony\\Component\\OptionsResolver\\' => array ( 0 => __DIR__ . '/..' . '/symfony/options-resolver', @@ -852,6 +867,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/SelectObjectValidator.php', + 'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => __DIR__ . '/../..' . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php', 'Combodo\\iTop\\Kernel' => __DIR__ . '/../..' . '/sources/Kernel.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php', @@ -2798,13 +2814,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\Form\\SubmitButton' => __DIR__ . '/..' . '/symfony/form/SubmitButton.php', 'Symfony\\Component\\Form\\SubmitButtonBuilder' => __DIR__ . '/..' . '/symfony/form/SubmitButtonBuilder.php', 'Symfony\\Component\\Form\\SubmitButtonTypeInterface' => __DIR__ . '/..' . '/symfony/form/SubmitButtonTypeInterface.php', - 'Symfony\\Component\\Form\\Test\\FormBuilderInterface' => __DIR__ . '/..' . '/symfony/form/Test/FormBuilderInterface.php', - 'Symfony\\Component\\Form\\Test\\FormIntegrationTestCase' => __DIR__ . '/..' . '/symfony/form/Test/FormIntegrationTestCase.php', - 'Symfony\\Component\\Form\\Test\\FormInterface' => __DIR__ . '/..' . '/symfony/form/Test/FormInterface.php', - 'Symfony\\Component\\Form\\Test\\FormPerformanceTestCase' => __DIR__ . '/..' . '/symfony/form/Test/FormPerformanceTestCase.php', - 'Symfony\\Component\\Form\\Test\\Traits\\RunTestTrait' => __DIR__ . '/..' . '/symfony/form/Test/Traits/RunTestTrait.php', - 'Symfony\\Component\\Form\\Test\\Traits\\ValidatorExtensionTrait' => __DIR__ . '/..' . '/symfony/form/Test/Traits/ValidatorExtensionTrait.php', - 'Symfony\\Component\\Form\\Test\\TypeTestCase' => __DIR__ . '/..' . '/symfony/form/Test/TypeTestCase.php', 'Symfony\\Component\\Form\\Util\\FormUtil' => __DIR__ . '/..' . '/symfony/form/Util/FormUtil.php', 'Symfony\\Component\\Form\\Util\\InheritDataAwareIterator' => __DIR__ . '/..' . '/symfony/form/Util/InheritDataAwareIterator.php', 'Symfony\\Component\\Form\\Util\\OptionsResolverWrapper' => __DIR__ . '/..' . '/symfony/form/Util/OptionsResolverWrapper.php', @@ -3205,6 +3214,24 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\OptionsResolver\\OptionConfigurator' => __DIR__ . '/..' . '/symfony/options-resolver/OptionConfigurator.php', 'Symfony\\Component\\OptionsResolver\\Options' => __DIR__ . '/..' . '/symfony/options-resolver/Options.php', 'Symfony\\Component\\OptionsResolver\\OptionsResolver' => __DIR__ . '/..' . '/symfony/options-resolver/OptionsResolver.php', + 'Symfony\\Component\\PasswordHasher\\Command\\UserPasswordHashCommand' => __DIR__ . '/..' . '/symfony/password-hasher/Command/UserPasswordHashCommand.php', + 'Symfony\\Component\\PasswordHasher\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/password-hasher/Exception/ExceptionInterface.php', + 'Symfony\\Component\\PasswordHasher\\Exception\\InvalidPasswordException' => __DIR__ . '/..' . '/symfony/password-hasher/Exception/InvalidPasswordException.php', + 'Symfony\\Component\\PasswordHasher\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/password-hasher/Exception/LogicException.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\CheckPasswordLengthTrait' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/CheckPasswordLengthTrait.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\MessageDigestPasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\MigratingPasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/MigratingPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\NativePasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/NativePasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherAwareInterface' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/PasswordHasherAwareInterface.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactory' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/PasswordHasherFactory.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactoryInterface' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/PasswordHasherFactoryInterface.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\Pbkdf2PasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/Pbkdf2PasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\PlaintextPasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/PlaintextPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\SodiumPasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/SodiumPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\UserPasswordHasher' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/UserPasswordHasher.php', + 'Symfony\\Component\\PasswordHasher\\Hasher\\UserPasswordHasherInterface' => __DIR__ . '/..' . '/symfony/password-hasher/Hasher/UserPasswordHasherInterface.php', + 'Symfony\\Component\\PasswordHasher\\LegacyPasswordHasherInterface' => __DIR__ . '/..' . '/symfony/password-hasher/LegacyPasswordHasherInterface.php', + 'Symfony\\Component\\PasswordHasher\\PasswordHasherInterface' => __DIR__ . '/..' . '/symfony/password-hasher/PasswordHasherInterface.php', 'Symfony\\Component\\PropertyAccess\\Exception\\AccessException' => __DIR__ . '/..' . '/symfony/property-access/Exception/AccessException.php', 'Symfony\\Component\\PropertyAccess\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/property-access/Exception/ExceptionInterface.php', 'Symfony\\Component\\PropertyAccess\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/property-access/Exception/InvalidArgumentException.php', @@ -3335,6 +3362,113 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Component\\Runtime\\Runner\\Symfony\\ResponseRunner' => __DIR__ . '/..' . '/symfony/runtime/Runner/Symfony/ResponseRunner.php', 'Symfony\\Component\\Runtime\\RuntimeInterface' => __DIR__ . '/..' . '/symfony/runtime/RuntimeInterface.php', 'Symfony\\Component\\Runtime\\SymfonyRuntime' => __DIR__ . '/..' . '/symfony/runtime/SymfonyRuntime.php', + 'Symfony\\Component\\Security\\Core\\AuthenticationEvents' => __DIR__ . '/..' . '/symfony/security-core/AuthenticationEvents.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationTrustResolver' => __DIR__ . '/..' . '/symfony/security-core/Authentication/AuthenticationTrustResolver.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationTrustResolverInterface' => __DIR__ . '/..' . '/symfony/security-core/Authentication/AuthenticationTrustResolverInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\CacheTokenVerifier' => __DIR__ . '/..' . '/symfony/security-core/Authentication/RememberMe/CacheTokenVerifier.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\InMemoryTokenProvider' => __DIR__ . '/..' . '/symfony/security-core/Authentication/RememberMe/InMemoryTokenProvider.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\PersistentToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/RememberMe/PersistentToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\PersistentTokenInterface' => __DIR__ . '/..' . '/symfony/security-core/Authentication/RememberMe/PersistentTokenInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\TokenProviderInterface' => __DIR__ . '/..' . '/symfony/security-core/Authentication/RememberMe/TokenProviderInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\TokenVerifierInterface' => __DIR__ . '/..' . '/symfony/security-core/Authentication/RememberMe/TokenVerifierInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/AbstractToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\NullToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/NullToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\PreAuthenticatedToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/PreAuthenticatedToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\RememberMeToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/RememberMeToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorage' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/Storage/TokenStorage.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/Storage/TokenStorageInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\UsageTrackingTokenStorage' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\SwitchUserToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/SwitchUserToken.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/TokenInterface.php', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken' => __DIR__ . '/..' . '/symfony/security-core/Authentication/Token/UsernamePasswordToken.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager' => __DIR__ . '/..' . '/symfony/security-core/Authorization/AccessDecisionManager.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface' => __DIR__ . '/..' . '/symfony/security-core/Authorization/AccessDecisionManagerInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker' => __DIR__ . '/..' . '/symfony/security-core/Authorization/AuthorizationChecker.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface' => __DIR__ . '/..' . '/symfony/security-core/Authorization/AuthorizationCheckerInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\ExpressionLanguage' => __DIR__ . '/..' . '/symfony/security-core/Authorization/ExpressionLanguage.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\ExpressionLanguageProvider' => __DIR__ . '/..' . '/symfony/security-core/Authorization/ExpressionLanguageProvider.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\AccessDecisionStrategyInterface' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Strategy/AccessDecisionStrategyInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\AffirmativeStrategy' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Strategy/AffirmativeStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\ConsensusStrategy' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Strategy/ConsensusStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\PriorityStrategy' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Strategy/PriorityStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Strategy\\UnanimousStrategy' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Strategy/UnanimousStrategy.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\TraceableAccessDecisionManager' => __DIR__ . '/..' . '/symfony/security-core/Authorization/TraceableAccessDecisionManager.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AuthenticatedVoter' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/AuthenticatedVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/CacheableVoterInterface.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/ExpressionVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleHierarchyVoter' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/RoleHierarchyVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/RoleVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\TraceableVoter' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/TraceableVoter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/Voter.php', + 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface' => __DIR__ . '/..' . '/symfony/security-core/Authorization/Voter/VoterInterface.php', + 'Symfony\\Component\\Security\\Core\\Event\\AuthenticationEvent' => __DIR__ . '/..' . '/symfony/security-core/Event/AuthenticationEvent.php', + 'Symfony\\Component\\Security\\Core\\Event\\AuthenticationSuccessEvent' => __DIR__ . '/..' . '/symfony/security-core/Event/AuthenticationSuccessEvent.php', + 'Symfony\\Component\\Security\\Core\\Event\\VoteEvent' => __DIR__ . '/..' . '/symfony/security-core/Event/VoteEvent.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AccessDeniedException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AccountExpiredException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AccountExpiredException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AccountStatusException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AccountStatusException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationCredentialsNotFoundException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AuthenticationCredentialsNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AuthenticationExpiredException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\AuthenticationServiceException' => __DIR__ . '/..' . '/symfony/security-core/Exception/AuthenticationServiceException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException' => __DIR__ . '/..' . '/symfony/security-core/Exception/BadCredentialsException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CookieTheftException' => __DIR__ . '/..' . '/symfony/security-core/Exception/CookieTheftException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CredentialsExpiredException' => __DIR__ . '/..' . '/symfony/security-core/Exception/CredentialsExpiredException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAccountStatusException' => __DIR__ . '/..' . '/symfony/security-core/Exception/CustomUserMessageAccountStatusException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException' => __DIR__ . '/..' . '/symfony/security-core/Exception/CustomUserMessageAuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\DisabledException' => __DIR__ . '/..' . '/symfony/security-core/Exception/DisabledException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/security-core/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Security\\Core\\Exception\\InsufficientAuthenticationException' => __DIR__ . '/..' . '/symfony/security-core/Exception/InsufficientAuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/security-core/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\InvalidCsrfTokenException' => __DIR__ . '/..' . '/symfony/security-core/Exception/InvalidCsrfTokenException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LazyResponseException' => __DIR__ . '/..' . '/symfony/security-core/Exception/LazyResponseException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LockedException' => __DIR__ . '/..' . '/symfony/security-core/Exception/LockedException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/security-core/Exception/LogicException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\LogoutException' => __DIR__ . '/..' . '/symfony/security-core/Exception/LogoutException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\ProviderNotFoundException' => __DIR__ . '/..' . '/symfony/security-core/Exception/ProviderNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/security-core/Exception/RuntimeException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\SessionUnavailableException' => __DIR__ . '/..' . '/symfony/security-core/Exception/SessionUnavailableException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\TokenNotFoundException' => __DIR__ . '/..' . '/symfony/security-core/Exception/TokenNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\TooManyLoginAttemptsAuthenticationException' => __DIR__ . '/..' . '/symfony/security-core/Exception/TooManyLoginAttemptsAuthenticationException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\UnsupportedUserException' => __DIR__ . '/..' . '/symfony/security-core/Exception/UnsupportedUserException.php', + 'Symfony\\Component\\Security\\Core\\Exception\\UserNotFoundException' => __DIR__ . '/..' . '/symfony/security-core/Exception/UserNotFoundException.php', + 'Symfony\\Component\\Security\\Core\\Role\\Role' => __DIR__ . '/..' . '/symfony/security-core/Role/Role.php', + 'Symfony\\Component\\Security\\Core\\Role\\RoleHierarchy' => __DIR__ . '/..' . '/symfony/security-core/Role/RoleHierarchy.php', + 'Symfony\\Component\\Security\\Core\\Role\\RoleHierarchyInterface' => __DIR__ . '/..' . '/symfony/security-core/Role/RoleHierarchyInterface.php', + 'Symfony\\Component\\Security\\Core\\Role\\SwitchUserRole' => __DIR__ . '/..' . '/symfony/security-core/Role/SwitchUserRole.php', + 'Symfony\\Component\\Security\\Core\\Security' => __DIR__ . '/..' . '/symfony/security-core/Security.php', + 'Symfony\\Component\\Security\\Core\\Signature\\Exception\\ExpiredSignatureException' => __DIR__ . '/..' . '/symfony/security-core/Signature/Exception/ExpiredSignatureException.php', + 'Symfony\\Component\\Security\\Core\\Signature\\Exception\\InvalidSignatureException' => __DIR__ . '/..' . '/symfony/security-core/Signature/Exception/InvalidSignatureException.php', + 'Symfony\\Component\\Security\\Core\\Signature\\ExpiredSignatureStorage' => __DIR__ . '/..' . '/symfony/security-core/Signature/ExpiredSignatureStorage.php', + 'Symfony\\Component\\Security\\Core\\Signature\\SignatureHasher' => __DIR__ . '/..' . '/symfony/security-core/Signature/SignatureHasher.php', + 'Symfony\\Component\\Security\\Core\\Test\\AccessDecisionStrategyTestCase' => __DIR__ . '/..' . '/symfony/security-core/Test/AccessDecisionStrategyTestCase.php', + 'Symfony\\Component\\Security\\Core\\User\\AttributesBasedUserProviderInterface' => __DIR__ . '/..' . '/symfony/security-core/User/AttributesBasedUserProviderInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\ChainUserChecker' => __DIR__ . '/..' . '/symfony/security-core/User/ChainUserChecker.php', + 'Symfony\\Component\\Security\\Core\\User\\ChainUserProvider' => __DIR__ . '/..' . '/symfony/security-core/User/ChainUserProvider.php', + 'Symfony\\Component\\Security\\Core\\User\\EquatableInterface' => __DIR__ . '/..' . '/symfony/security-core/User/EquatableInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\InMemoryUser' => __DIR__ . '/..' . '/symfony/security-core/User/InMemoryUser.php', + 'Symfony\\Component\\Security\\Core\\User\\InMemoryUserChecker' => __DIR__ . '/..' . '/symfony/security-core/User/InMemoryUserChecker.php', + 'Symfony\\Component\\Security\\Core\\User\\InMemoryUserProvider' => __DIR__ . '/..' . '/symfony/security-core/User/InMemoryUserProvider.php', + 'Symfony\\Component\\Security\\Core\\User\\LegacyPasswordAuthenticatedUserInterface' => __DIR__ . '/..' . '/symfony/security-core/User/LegacyPasswordAuthenticatedUserInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\MissingUserProvider' => __DIR__ . '/..' . '/symfony/security-core/User/MissingUserProvider.php', + 'Symfony\\Component\\Security\\Core\\User\\OidcUser' => __DIR__ . '/..' . '/symfony/security-core/User/OidcUser.php', + 'Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface' => __DIR__ . '/..' . '/symfony/security-core/User/PasswordAuthenticatedUserInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface' => __DIR__ . '/..' . '/symfony/security-core/User/PasswordUpgraderInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\UserCheckerInterface' => __DIR__ . '/..' . '/symfony/security-core/User/UserCheckerInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\UserInterface' => __DIR__ . '/..' . '/symfony/security-core/User/UserInterface.php', + 'Symfony\\Component\\Security\\Core\\User\\UserProviderInterface' => __DIR__ . '/..' . '/symfony/security-core/User/UserProviderInterface.php', + 'Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPassword' => __DIR__ . '/..' . '/symfony/security-core/Validator/Constraints/UserPassword.php', + 'Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPasswordValidator' => __DIR__ . '/..' . '/symfony/security-core/Validator/Constraints/UserPasswordValidator.php', + 'Symfony\\Component\\Security\\Csrf\\CsrfToken' => __DIR__ . '/..' . '/symfony/security-csrf/CsrfToken.php', + 'Symfony\\Component\\Security\\Csrf\\CsrfTokenManager' => __DIR__ . '/..' . '/symfony/security-csrf/CsrfTokenManager.php', + 'Symfony\\Component\\Security\\Csrf\\CsrfTokenManagerInterface' => __DIR__ . '/..' . '/symfony/security-csrf/CsrfTokenManagerInterface.php', + 'Symfony\\Component\\Security\\Csrf\\Exception\\TokenNotFoundException' => __DIR__ . '/..' . '/symfony/security-csrf/Exception/TokenNotFoundException.php', + 'Symfony\\Component\\Security\\Csrf\\TokenGenerator\\TokenGeneratorInterface' => __DIR__ . '/..' . '/symfony/security-csrf/TokenGenerator/TokenGeneratorInterface.php', + 'Symfony\\Component\\Security\\Csrf\\TokenGenerator\\UriSafeTokenGenerator' => __DIR__ . '/..' . '/symfony/security-csrf/TokenGenerator/UriSafeTokenGenerator.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\ClearableTokenStorageInterface' => __DIR__ . '/..' . '/symfony/security-csrf/TokenStorage/ClearableTokenStorageInterface.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\NativeSessionTokenStorage' => __DIR__ . '/..' . '/symfony/security-csrf/TokenStorage/NativeSessionTokenStorage.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\SessionTokenStorage' => __DIR__ . '/..' . '/symfony/security-csrf/TokenStorage/SessionTokenStorage.php', + 'Symfony\\Component\\Security\\Csrf\\TokenStorage\\TokenStorageInterface' => __DIR__ . '/..' . '/symfony/security-csrf/TokenStorage/TokenStorageInterface.php', 'Symfony\\Component\\Stopwatch\\Section' => __DIR__ . '/..' . '/symfony/stopwatch/Section.php', 'Symfony\\Component\\Stopwatch\\Stopwatch' => __DIR__ . '/..' . '/symfony/stopwatch/Stopwatch.php', 'Symfony\\Component\\Stopwatch\\StopwatchEvent' => __DIR__ . '/..' . '/symfony/stopwatch/StopwatchEvent.php', diff --git a/lib/composer/installed.json b/lib/composer/installed.json index ff4ef935ef..42b10688ae 100644 --- a/lib/composer/installed.json +++ b/lib/composer/installed.json @@ -3654,6 +3654,85 @@ ], "install-path": "../symfony/options-resolver" }, + { + "name": "symfony/password-hasher", + "version": "v6.4.24", + "version_normalized": "6.4.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/dcab5ac87450aaed26483ba49c2ce86808da7557", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "time": "2025-07-10T08:14:14+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/password-hasher" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -4618,6 +4697,174 @@ ], "install-path": "../symfony/runtime" }, + { + "name": "symfony/security-core", + "version": "v6.4.26", + "version_normalized": "6.4.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0", + "reference": "8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" + }, + "time": "2025-09-02T19:15:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/security-core" + }, + { + "name": "symfony/security-csrf", + "version": "v6.4.24", + "version_normalized": "6.4.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "9a1efc8c10b86bcedc9233affd10c716b54ca1b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/9a1efc8c10b86bcedc9233affd10c716b54ca1b7", + "reference": "9a1efc8c10b86bcedc9233affd10c716b54ca1b7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-foundation": "^5.4|^6.0|^7.0" + }, + "time": "2025-07-10T08:14:14+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/security-csrf" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 0a7711456a..803afc5db1 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'f0ff1c868e9d60303d5bebe1e59c889d22df6706', + 'reference' => '29f05fefe6f5d4f4ca70729400221fc7b1cc0303', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'f0ff1c868e9d60303d5bebe1e59c889d22df6706', + 'reference' => '29f05fefe6f5d4f4ca70729400221fc7b1cc0303', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -510,6 +510,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/password-hasher' => array( + 'pretty_version' => 'v6.4.24', + 'version' => '6.4.24.0', + 'reference' => 'dcab5ac87450aaed26483ba49c2ce86808da7557', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/password-hasher', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/polyfill-ctype' => array( 'pretty_version' => 'v1.33.0', 'version' => '1.33.0.0', @@ -609,6 +618,24 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/security-core' => array( + 'pretty_version' => 'v6.4.26', + 'version' => '6.4.26.0', + 'reference' => '8b7c95bf04d82fcd0c06a918b2d849bfb2ab9cc0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/security-core', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/security-csrf' => array( + 'pretty_version' => 'v6.4.24', + 'version' => '6.4.24.0', + 'reference' => '9a1efc8c10b86bcedc9233affd10c716b54ca1b7', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/security-csrf', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/service-contracts' => array( 'pretty_version' => 'v3.6.0', 'version' => '3.6.0.0', diff --git a/lib/symfony/password-hasher/CHANGELOG.md b/lib/symfony/password-hasher/CHANGELOG.md new file mode 100644 index 0000000000..8258c30b8e --- /dev/null +++ b/lib/symfony/password-hasher/CHANGELOG.md @@ -0,0 +1,13 @@ +CHANGELOG +========= + +6.2 +--- + + * Use `SensitiveParameter` attribute to redact sensitive values in back traces + +5.3 +--- + + * Add the component + * Use `bcrypt` as default algorithm in `NativePasswordHasher` diff --git a/lib/symfony/password-hasher/Command/UserPasswordHashCommand.php b/lib/symfony/password-hasher/Command/UserPasswordHashCommand.php new file mode 100644 index 0000000000..be2a8025d8 --- /dev/null +++ b/lib/symfony/password-hasher/Command/UserPasswordHashCommand.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +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\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +/** + * Hashes a user's password. + * + * @author Sarah Khalil + * @author Robin Chalas + * + * @final + */ +#[AsCommand(name: 'security:hash-password', description: 'Hash a user password')] +class UserPasswordHashCommand extends Command +{ + private PasswordHasherFactoryInterface $hasherFactory; + private array $userClasses; + + public function __construct(PasswordHasherFactoryInterface $hasherFactory, array $userClasses = []) + { + $this->hasherFactory = $hasherFactory; + $this->userClasses = $userClasses; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('password', InputArgument::OPTIONAL, 'The plain password to hash.') + ->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the hasher used to hash the password.') + ->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the hasher generate one.') + ->setHelp(<<%command.name% command hashes passwords according to your +security configuration. This command is mainly used to generate passwords for +the in_memory user provider type and for changing passwords +in the database while developing the application. + +Suppose that you have the following security configuration in your application: + + +# config/packages/security.yml +security: + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + App\Entity\User: auto + + +If you execute the command non-interactively, the first available configured +user class under the security.password_hashers key is used and a random salt is +generated to hash the password: + + php %command.full_name% --no-interaction [password] + +Pass the full user class path as the second argument to hash passwords for +your own entities: + + php %command.full_name% --no-interaction [password] 'App\Entity\User' + +Executing the command interactively allows you to generate a random salt for +hashing the password: + + php %command.full_name% [password] 'App\Entity\User' + +In case your hasher doesn't require a salt, add the empty-salt option: + + php %command.full_name% --empty-salt [password] 'App\Entity\User' + +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io; + + $input->isInteractive() ? $errorIo->title('Symfony Password Hash Utility') : $errorIo->newLine(); + + $password = $input->getArgument('password'); + $userClass = $this->getUserClass($input, $io); + $emptySalt = $input->getOption('empty-salt'); + + $hasher = $this->hasherFactory->getPasswordHasher($userClass); + $saltlessWithoutEmptySalt = !$emptySalt && !$hasher instanceof LegacyPasswordHasherInterface; + + if ($saltlessWithoutEmptySalt) { + $emptySalt = true; + } + + if (!$password) { + if (!$input->isInteractive()) { + $errorIo->error('The password must not be empty.'); + + return 1; + } + $passwordQuestion = $this->createPasswordQuestion(); + $password = $errorIo->askQuestion($passwordQuestion); + } + + $salt = null; + + if ($input->isInteractive() && !$emptySalt) { + $emptySalt = true; + + $errorIo->note('The command will take care of generating a salt for you. Be aware that some hashers advise to let them generate their own salt. If you\'re using one of those hashers, please answer \'no\' to the question below. '.\PHP_EOL.'Provide the \'empty-salt\' option in order to let the hasher handle the generation itself.'); + + if ($errorIo->confirm('Confirm salt generation ?')) { + $salt = $this->generateSalt(); + $emptySalt = false; + } + } elseif (!$emptySalt) { + $salt = $this->generateSalt(); + } + + $hashedPassword = $hasher->hash($password, $salt); + + $rows = [ + ['Hasher used', $hasher::class], + ['Password hash', $hashedPassword], + ]; + if (!$emptySalt) { + $rows[] = ['Generated salt', $salt]; + } + $io->table(['Key', 'Value'], $rows); + + if (!$emptySalt) { + $errorIo->note(\sprintf('Make sure that your salt storage field fits the salt length: %s chars', \strlen($salt))); + } elseif ($saltlessWithoutEmptySalt) { + $errorIo->note('Self-salting hasher used: the hasher generated its own built-in salt.'); + } + + $errorIo->success('Password hashing succeeded'); + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('user-class')) { + $suggestions->suggestValues($this->userClasses); + + return; + } + } + + /** + * Create the password question to ask the user for the password to be hashed. + */ + private function createPasswordQuestion(): Question + { + $passwordQuestion = new Question('Type in your password to be hashed'); + + return $passwordQuestion->setValidator(function ($value) { + if ('' === trim($value)) { + throw new InvalidArgumentException('The password must not be empty.'); + } + + return $value; + })->setHidden(true)->setMaxAttempts(20); + } + + private function generateSalt(): string + { + return base64_encode(random_bytes(30)); + } + + private function getUserClass(InputInterface $input, SymfonyStyle $io): string + { + if (null !== $userClass = $input->getArgument('user-class')) { + return $userClass; + } + + if (!$this->userClasses) { + throw new RuntimeException('There are no configured password hashers for the "security" extension.'); + } + + if (!$input->isInteractive() || 1 === \count($this->userClasses)) { + return reset($this->userClasses); + } + + $userClasses = $this->userClasses; + natcasesort($userClasses); + $userClasses = array_values($userClasses); + + return $io->choice('For which user class would you like to hash a password?', $userClasses, reset($userClasses)); + } +} diff --git a/lib/symfony/password-hasher/Exception/ExceptionInterface.php b/lib/symfony/password-hasher/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..2d80d8a78f --- /dev/null +++ b/lib/symfony/password-hasher/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Exception; + +/** + * Interface for exceptions thrown by the password-hasher component. + * + * @author Robin Chalas + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/lib/symfony/password-hasher/Exception/InvalidPasswordException.php b/lib/symfony/password-hasher/Exception/InvalidPasswordException.php new file mode 100644 index 0000000000..c70a4d5561 --- /dev/null +++ b/lib/symfony/password-hasher/Exception/InvalidPasswordException.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\PasswordHasher\Exception; + +/** + * @author Robin Chalas + */ +class InvalidPasswordException extends \RuntimeException implements ExceptionInterface +{ + public function __construct(string $message = 'Invalid password.', int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/lib/symfony/password-hasher/Exception/LogicException.php b/lib/symfony/password-hasher/Exception/LogicException.php new file mode 100644 index 0000000000..f4d9f31ff5 --- /dev/null +++ b/lib/symfony/password-hasher/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Exception; + +/** + * @author Robin Chalas + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/lib/symfony/password-hasher/Hasher/CheckPasswordLengthTrait.php b/lib/symfony/password-hasher/Hasher/CheckPasswordLengthTrait.php new file mode 100644 index 0000000000..9721b4182d --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/CheckPasswordLengthTrait.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\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\PasswordHasherInterface; + +/** + * @author Robin Chalas + */ +trait CheckPasswordLengthTrait +{ + private function isPasswordTooLong(#[\SensitiveParameter] string $password): bool + { + return PasswordHasherInterface::MAX_PASSWORD_LENGTH < \strlen($password); + } +} diff --git a/lib/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php b/lib/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php new file mode 100644 index 0000000000..d6dccf4151 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\Exception\LogicException; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +/** + * MessageDigestPasswordHasher uses a message digest algorithm. + * + * @author Fabien Potencier + */ +class MessageDigestPasswordHasher implements LegacyPasswordHasherInterface +{ + use CheckPasswordLengthTrait; + + private string $algorithm; + private bool $encodeHashAsBase64; + private int $iterations = 1; + private int $hashLength = -1; + + /** + * @param string $algorithm The digest algorithm to use + * @param bool $encodeHashAsBase64 Whether to base64 encode the password hash + * @param int $iterations The number of iterations to use to stretch the password hash + */ + public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 5000) + { + $this->algorithm = $algorithm; + $this->encodeHashAsBase64 = $encodeHashAsBase64; + + try { + $this->hashLength = \strlen($this->hash('', 'salt')); + } catch (\LogicException) { + // ignore algorithm not supported + } + + $this->iterations = $iterations; + } + + public function hash(#[\SensitiveParameter] string $plainPassword, ?string $salt = null): string + { + if ($this->isPasswordTooLong($plainPassword)) { + throw new InvalidPasswordException(); + } + + if (!\in_array($this->algorithm, hash_algos(), true)) { + throw new LogicException(\sprintf('The algorithm "%s" is not supported.', $this->algorithm)); + } + + $salted = $this->mergePasswordAndSalt($plainPassword, $salt); + $digest = hash($this->algorithm, $salted, true); + + // "stretch" hash + for ($i = 1; $i < $this->iterations; ++$i) { + $digest = hash($this->algorithm, $digest.$salted, true); + } + + return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); + } + + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword, ?string $salt = null): bool + { + if (\strlen($hashedPassword) !== $this->hashLength || str_contains($hashedPassword, '$')) { + return false; + } + + return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt)); + } + + public function needsRehash(string $hashedPassword): bool + { + return false; + } + + private function mergePasswordAndSalt(#[\SensitiveParameter] string $password, ?string $salt): string + { + if (!$salt) { + return $password; + } + + if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) { + throw new \InvalidArgumentException('Cannot use { or } in salt.'); + } + + return $password.'{'.$salt.'}'; + } +} diff --git a/lib/symfony/password-hasher/Hasher/MigratingPasswordHasher.php b/lib/symfony/password-hasher/Hasher/MigratingPasswordHasher.php new file mode 100644 index 0000000000..639ffc8eec --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/MigratingPasswordHasher.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\PasswordHasherInterface; + +/** + * Hashes passwords using the best available hasher. + * Verifies them using a chain of hashers. + * + * /!\ Don't put a PlaintextPasswordHasher in the list as that'd mean a leaked hash + * could be used to authenticate successfully without knowing the cleartext password. + * + * @author Nicolas Grekas + */ +final class MigratingPasswordHasher implements PasswordHasherInterface +{ + private PasswordHasherInterface $bestHasher; + private array $extraHashers; + + public function __construct(PasswordHasherInterface $bestHasher, PasswordHasherInterface ...$extraHashers) + { + $this->bestHasher = $bestHasher; + $this->extraHashers = $extraHashers; + } + + public function hash(#[\SensitiveParameter] string $plainPassword, ?string $salt = null): string + { + return $this->bestHasher->hash($plainPassword, $salt); + } + + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword, ?string $salt = null): bool + { + if ($this->bestHasher->verify($hashedPassword, $plainPassword, $salt)) { + return true; + } + + if (!$this->bestHasher->needsRehash($hashedPassword)) { + return false; + } + + foreach ($this->extraHashers as $hasher) { + if ($hasher->verify($hashedPassword, $plainPassword, $salt)) { + return true; + } + } + + return false; + } + + public function needsRehash(string $hashedPassword): bool + { + return $this->bestHasher->needsRehash($hashedPassword); + } +} diff --git a/lib/symfony/password-hasher/Hasher/NativePasswordHasher.php b/lib/symfony/password-hasher/Hasher/NativePasswordHasher.php new file mode 100644 index 0000000000..413bcecf32 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/NativePasswordHasher.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; + +/** + * Hashes passwords using password_hash(). + * + * @author Elnur Abdurrakhimov + * @author Terje Bråten + * @author Nicolas Grekas + */ +final class NativePasswordHasher implements PasswordHasherInterface +{ + use CheckPasswordLengthTrait; + + private string $algorithm = \PASSWORD_BCRYPT; + private array $options; + + /** + * @param string|null $algorithm An algorithm supported by password_hash() or null to use the best available algorithm + */ + public function __construct(?int $opsLimit = null, ?int $memLimit = null, ?int $cost = null, ?string $algorithm = null) + { + $cost ??= 13; + $opsLimit ??= max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4); + $memLimit ??= max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024); + + if (3 > $opsLimit) { + throw new \InvalidArgumentException('$opsLimit must be 3 or greater.'); + } + + if (10 * 1024 > $memLimit) { + throw new \InvalidArgumentException('$memLimit must be 10k or greater.'); + } + + if ($cost < 4 || 31 < $cost) { + throw new \InvalidArgumentException('$cost must be in the range of 4-31.'); + } + + if (null !== $algorithm) { + $algorithms = [1 => \PASSWORD_BCRYPT, '2y' => \PASSWORD_BCRYPT]; + + if (\defined('PASSWORD_ARGON2I')) { + $algorithms[2] = $algorithms['argon2i'] = \PASSWORD_ARGON2I; + } + + if (\defined('PASSWORD_ARGON2ID')) { + $algorithms[3] = $algorithms['argon2id'] = \PASSWORD_ARGON2ID; + } + + $this->algorithm = $algorithms[$algorithm] ?? $algorithm; + } + + $this->options = [ + 'cost' => $cost, + 'time_cost' => $opsLimit, + 'memory_cost' => $memLimit >> 10, + 'threads' => 1, + ]; + } + + public function hash(#[\SensitiveParameter] string $plainPassword): string + { + if ($this->isPasswordTooLong($plainPassword)) { + throw new InvalidPasswordException(); + } + + if (\PASSWORD_BCRYPT === $this->algorithm && (72 < \strlen($plainPassword) || str_contains($plainPassword, "\0"))) { + $plainPassword = base64_encode(hash('sha512', $plainPassword, true)); + } + + return password_hash($plainPassword, $this->algorithm, $this->options); + } + + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword): bool + { + if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) { + return false; + } + + if (!str_starts_with($hashedPassword, '$argon')) { + // Bcrypt cuts on NUL chars and after 72 bytes + if (str_starts_with($hashedPassword, '$2') && (72 < \strlen($plainPassword) || str_contains($plainPassword, "\0"))) { + $plainPassword = base64_encode(hash('sha512', $plainPassword, true)); + } + + return password_verify($plainPassword, $hashedPassword); + } + + if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) { + return sodium_crypto_pwhash_str_verify($hashedPassword, $plainPassword); + } + + if (\extension_loaded('libsodium') && version_compare(phpversion('libsodium'), '1.0.14', '>=')) { + return \Sodium\crypto_pwhash_str_verify($hashedPassword, $plainPassword); + } + + return password_verify($plainPassword, $hashedPassword); + } + + public function needsRehash(string $hashedPassword): bool + { + return password_needs_rehash($hashedPassword, $this->algorithm, $this->options); + } +} diff --git a/lib/symfony/password-hasher/Hasher/PasswordHasherAwareInterface.php b/lib/symfony/password-hasher/Hasher/PasswordHasherAwareInterface.php new file mode 100644 index 0000000000..58046bc56c --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/PasswordHasherAwareInterface.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\PasswordHasher\Hasher; + +/** + * @author Christophe Coevoet + */ +interface PasswordHasherAwareInterface +{ + /** + * Gets the name of the password hasher used to hash the password. + * + * If the method returns null, the standard way to retrieve the password hasher + * will be used instead. + */ + public function getPasswordHasherName(): ?string; +} diff --git a/lib/symfony/password-hasher/Hasher/PasswordHasherFactory.php b/lib/symfony/password-hasher/Hasher/PasswordHasherFactory.php new file mode 100644 index 0000000000..dca82678ad --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/PasswordHasherFactory.php @@ -0,0 +1,240 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\Exception\LogicException; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +/** + * A generic hasher factory implementation. + * + * @author Nicolas Grekas + * @author Robin Chalas + */ +class PasswordHasherFactory implements PasswordHasherFactoryInterface +{ + private array $passwordHashers; + + /** + * @param array $passwordHashers + */ + public function __construct(array $passwordHashers) + { + $this->passwordHashers = $passwordHashers; + } + + public function getPasswordHasher(string|PasswordAuthenticatedUserInterface|PasswordHasherAwareInterface $user): PasswordHasherInterface + { + $hasherKey = null; + + if ($user instanceof PasswordHasherAwareInterface && null !== $hasherName = $user->getPasswordHasherName()) { + if (!\array_key_exists($hasherName, $this->passwordHashers)) { + throw new \RuntimeException(\sprintf('The password hasher "%s" was not configured.', $hasherName)); + } + + $hasherKey = $hasherName; + } else { + foreach ($this->passwordHashers as $class => $hasher) { + if ((\is_object($user) && $user instanceof $class) || (!\is_object($user) && (is_subclass_of($user, $class) || $user == $class))) { + $hasherKey = $class; + break; + } + } + } + + if (null === $hasherKey) { + throw new \RuntimeException(\sprintf('No password hasher has been configured for account "%s".', \is_object($user) ? get_debug_type($user) : $user)); + } + + if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) { + $this->passwordHashers[$hasherKey] = $this->createHasher($this->passwordHashers[$hasherKey]); + } + + return $this->passwordHashers[$hasherKey]; + } + + /** + * Creates the actual hasher instance. + * + * @throws \InvalidArgumentException + */ + private function createHasher(array $config, bool $isExtra = false): PasswordHasherInterface + { + if (isset($config['instance'])) { + if (!isset($config['migrate_from'])) { + return $config['instance']; + } + + $config = $this->getMigratingPasswordConfig($config); + } + + if (isset($config['algorithm'])) { + $rawConfig = $config; + $config = $this->getHasherConfigFromAlgorithm($config); + } + if (!isset($config['class'])) { + throw new \InvalidArgumentException('"class" must be set in '.json_encode($config)); + } + if (!isset($config['arguments'])) { + throw new \InvalidArgumentException('"arguments" must be set in '.json_encode($config)); + } + + $hasher = new $config['class'](...$config['arguments']); + + if ($isExtra || !\in_array($config['class'], [NativePasswordHasher::class, SodiumPasswordHasher::class], true)) { + return $hasher; + } + + if ($rawConfig ?? null) { + $extrapasswordHashers = array_map(function (string $algo) use ($rawConfig): PasswordHasherInterface { + $rawConfig['algorithm'] = $algo; + + return $this->createHasher($rawConfig); + }, ['pbkdf2', $rawConfig['hash_algorithm'] ?? 'sha512']); + } else { + $extrapasswordHashers = [new Pbkdf2PasswordHasher(), new MessageDigestPasswordHasher()]; + } + + return new MigratingPasswordHasher($hasher, ...$extrapasswordHashers); + } + + private function getHasherConfigFromAlgorithm(array $config): array + { + if ('auto' === $config['algorithm']) { + // "plaintext" is not listed as any leaked hashes could then be used to authenticate directly + if (SodiumPasswordHasher::isSupported()) { + $algorithms = ['native', 'sodium', 'pbkdf2']; + } else { + $algorithms = ['native', 'pbkdf2']; + } + + if ($config['hash_algorithm'] ?? '') { + $algorithms[] = $config['hash_algorithm']; + } + + $hasherChain = []; + foreach ($algorithms as $algorithm) { + $config['algorithm'] = $algorithm; + $hasherChain[] = $this->createHasher($config, true); + } + + return [ + 'class' => MigratingPasswordHasher::class, + 'arguments' => $hasherChain, + ]; + } + + if ($config['migrate_from'] ?? false) { + return $this->getMigratingPasswordConfig($config); + } + + switch ($config['algorithm']) { + case 'plaintext': + return [ + 'class' => PlaintextPasswordHasher::class, + 'arguments' => [$config['ignore_case'] ?? false], + ]; + + case 'pbkdf2': + return [ + 'class' => Pbkdf2PasswordHasher::class, + 'arguments' => [ + $config['hash_algorithm'] ?? 'sha512', + $config['encode_as_base64'] ?? true, + $config['iterations'] ?? 1000, + $config['key_length'] ?? 40, + ], + ]; + + case 'bcrypt': + $config['algorithm'] = 'native'; + $config['native_algorithm'] = \PASSWORD_BCRYPT; + + return $this->getHasherConfigFromAlgorithm($config); + + case 'native': + return [ + 'class' => NativePasswordHasher::class, + 'arguments' => [ + $config['time_cost'] ?? null, + (($config['memory_cost'] ?? 0) << 10) ?: null, + $config['cost'] ?? null, + ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []), + ]; + + case 'sodium': + return [ + 'class' => SodiumPasswordHasher::class, + 'arguments' => [ + $config['time_cost'] ?? null, + (($config['memory_cost'] ?? 0) << 10) ?: null, + ], + ]; + + case 'argon2i': + if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $config['algorithm'] = 'sodium'; + } elseif (\defined('PASSWORD_ARGON2I')) { + $config['algorithm'] = 'native'; + $config['native_algorithm'] = \PASSWORD_ARGON2I; + } else { + throw new LogicException(\sprintf('Algorithm "argon2i" is not available. Use "%s" instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id" or "auto' : 'auto')); + } + + return $this->getHasherConfigFromAlgorithm($config); + + case 'argon2id': + if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $config['algorithm'] = 'sodium'; + } elseif (\defined('PASSWORD_ARGON2ID')) { + $config['algorithm'] = 'native'; + $config['native_algorithm'] = \PASSWORD_ARGON2ID; + } else { + throw new LogicException(\sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto')); + } + + return $this->getHasherConfigFromAlgorithm($config); + } + + return [ + 'class' => MessageDigestPasswordHasher::class, + 'arguments' => [ + $config['algorithm'], + $config['encode_as_base64'] ?? true, + $config['iterations'] ?? 5000, + ], + ]; + } + + private function getMigratingPasswordConfig(array $config): array + { + $frompasswordHashers = $config['migrate_from']; + unset($config['migrate_from']); + $hasherChain = [$this->createHasher($config, true)]; + + foreach ($frompasswordHashers as $name) { + if ($hasher = $this->passwordHashers[$name] ?? false) { + $hasher = $hasher instanceof PasswordHasherInterface ? $hasher : $this->createHasher($hasher, true); + } else { + $hasher = $this->createHasher(['algorithm' => $name], true); + } + + $hasherChain[] = $hasher; + } + + return [ + 'class' => MigratingPasswordHasher::class, + 'arguments' => $hasherChain, + ]; + } +} diff --git a/lib/symfony/password-hasher/Hasher/PasswordHasherFactoryInterface.php b/lib/symfony/password-hasher/Hasher/PasswordHasherFactoryInterface.php new file mode 100644 index 0000000000..6dc158f8e1 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/PasswordHasherFactoryInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +/** + * PasswordHasherFactoryInterface to support different password hashers for different user accounts. + * + * @author Robin Chalas + * @author Johannes M. Schmitt + */ +interface PasswordHasherFactoryInterface +{ + /** + * Returns the password hasher to use for the given user. + * + * @throws \RuntimeException When no password hasher could be found for the user + */ + public function getPasswordHasher(string|PasswordAuthenticatedUserInterface|PasswordHasherAwareInterface $user): PasswordHasherInterface; +} diff --git a/lib/symfony/password-hasher/Hasher/Pbkdf2PasswordHasher.php b/lib/symfony/password-hasher/Hasher/Pbkdf2PasswordHasher.php new file mode 100644 index 0000000000..fd61882de9 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/Pbkdf2PasswordHasher.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\Exception\LogicException; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +/** + * Pbkdf2PasswordHasher uses the PBKDF2 (Password-Based Key Derivation Function 2). + * + * Providing a high level of Cryptographic security, + * PBKDF2 is recommended by the National Institute of Standards and Technology (NIST). + * + * But also warrants a warning, using PBKDF2 (with a high number of iterations) slows down the process. + * PBKDF2 should be used with caution and care. + * + * @author Sebastiaan Stok + * @author Andrew Johnson + * @author Fabien Potencier + */ +final class Pbkdf2PasswordHasher implements LegacyPasswordHasherInterface +{ + use CheckPasswordLengthTrait; + + private string $algorithm; + private bool $encodeHashAsBase64; + private int $iterations = 1; + private int $length; + private int $encodedLength = -1; + + /** + * @param string $algorithm The digest algorithm to use + * @param bool $encodeHashAsBase64 Whether to base64 encode the password hash + * @param int $iterations The number of iterations to use to stretch the password hash + * @param int $length Length of derived key to create + */ + public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 1000, int $length = 40) + { + $this->algorithm = $algorithm; + $this->encodeHashAsBase64 = $encodeHashAsBase64; + $this->length = $length; + + try { + $this->encodedLength = \strlen($this->hash('', 'salt')); + } catch (\LogicException) { + // ignore unsupported algorithm + } + + $this->iterations = $iterations; + } + + public function hash(#[\SensitiveParameter] string $plainPassword, ?string $salt = null): string + { + if ($this->isPasswordTooLong($plainPassword)) { + throw new InvalidPasswordException(); + } + + if (!\in_array($this->algorithm, hash_algos(), true)) { + throw new LogicException(\sprintf('The algorithm "%s" is not supported.', $this->algorithm)); + } + + $digest = hash_pbkdf2($this->algorithm, $plainPassword, $salt ?? '', $this->iterations, $this->length, true); + + return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); + } + + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword, ?string $salt = null): bool + { + if (\strlen($hashedPassword) !== $this->encodedLength || str_contains($hashedPassword, '$')) { + return false; + } + + return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt)); + } + + public function needsRehash(string $hashedPassword): bool + { + return false; + } +} diff --git a/lib/symfony/password-hasher/Hasher/PlaintextPasswordHasher.php b/lib/symfony/password-hasher/Hasher/PlaintextPasswordHasher.php new file mode 100644 index 0000000000..cffb6340a0 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/PlaintextPasswordHasher.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +/** + * PlaintextPasswordHasher does not do any hashing but is useful in testing environments. + * + * As this hasher is not cryptographically secure, usage of it in production environments is discouraged. + * + * @author Fabien Potencier + */ +class PlaintextPasswordHasher implements LegacyPasswordHasherInterface +{ + use CheckPasswordLengthTrait; + + private bool $ignorePasswordCase; + + /** + * @param bool $ignorePasswordCase Compare password case-insensitive + */ + public function __construct(bool $ignorePasswordCase = false) + { + $this->ignorePasswordCase = $ignorePasswordCase; + } + + public function hash(#[\SensitiveParameter] string $plainPassword, ?string $salt = null): string + { + if ($this->isPasswordTooLong($plainPassword)) { + throw new InvalidPasswordException(); + } + + return $this->mergePasswordAndSalt($plainPassword, $salt); + } + + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword, ?string $salt = null): bool + { + if ($this->isPasswordTooLong($plainPassword)) { + return false; + } + + $pass2 = $this->mergePasswordAndSalt($plainPassword, $salt); + + if (!$this->ignorePasswordCase) { + return hash_equals($hashedPassword, $pass2); + } + + return hash_equals(strtolower($hashedPassword), strtolower($pass2)); + } + + public function needsRehash(string $hashedPassword): bool + { + return false; + } + + private function mergePasswordAndSalt(#[\SensitiveParameter] string $password, ?string $salt): string + { + if (empty($salt)) { + return $password; + } + + if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) { + throw new \InvalidArgumentException('Cannot use { or } in salt.'); + } + + return $password.'{'.$salt.'}'; + } +} diff --git a/lib/symfony/password-hasher/Hasher/SodiumPasswordHasher.php b/lib/symfony/password-hasher/Hasher/SodiumPasswordHasher.php new file mode 100644 index 0000000000..ae6c03fdb6 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/SodiumPasswordHasher.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\Exception\LogicException; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; + +/** + * Hashes passwords using libsodium. + * + * @author Robin Chalas + * @author Zan Baldwin + * @author Dominik Müller + */ +final class SodiumPasswordHasher implements PasswordHasherInterface +{ + use CheckPasswordLengthTrait; + + private int $opsLimit; + private int $memLimit; + + public function __construct(?int $opsLimit = null, ?int $memLimit = null) + { + if (!self::isSupported()) { + throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.'); + } + + $this->opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4); + $this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024); + + if (3 > $this->opsLimit) { + throw new \InvalidArgumentException('$opsLimit must be 3 or greater.'); + } + + if (10 * 1024 > $this->memLimit) { + throw new \InvalidArgumentException('$memLimit must be 10k or greater.'); + } + } + + public static function isSupported(): bool + { + return version_compare(\extension_loaded('sodium') ? \SODIUM_LIBRARY_VERSION : phpversion('libsodium'), '1.0.14', '>='); + } + + public function hash(#[\SensitiveParameter] string $plainPassword): string + { + if ($this->isPasswordTooLong($plainPassword)) { + throw new InvalidPasswordException(); + } + + if (\function_exists('sodium_crypto_pwhash_str')) { + return sodium_crypto_pwhash_str($plainPassword, $this->opsLimit, $this->memLimit); + } + + if (\extension_loaded('libsodium')) { + return \Sodium\crypto_pwhash_str($plainPassword, $this->opsLimit, $this->memLimit); + } + + throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.'); + } + + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword): bool + { + if ('' === $plainPassword) { + return false; + } + + if ($this->isPasswordTooLong($plainPassword)) { + return false; + } + + if (!str_starts_with($hashedPassword, '$argon')) { + if (str_starts_with($hashedPassword, '$2') && (72 < \strlen($plainPassword) || str_contains($plainPassword, "\0"))) { + $plainPassword = base64_encode(hash('sha512', $plainPassword, true)); + } + + // Accept validating non-argon passwords for seamless migrations + return password_verify($plainPassword, $hashedPassword); + } + + if (\function_exists('sodium_crypto_pwhash_str_verify')) { + return sodium_crypto_pwhash_str_verify($hashedPassword, $plainPassword); + } + + if (\extension_loaded('libsodium')) { + return \Sodium\crypto_pwhash_str_verify($hashedPassword, $plainPassword); + } + + return false; + } + + public function needsRehash(string $hashedPassword): bool + { + if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) { + return sodium_crypto_pwhash_str_needs_rehash($hashedPassword, $this->opsLimit, $this->memLimit); + } + + if (\extension_loaded('libsodium')) { + return \Sodium\crypto_pwhash_str_needs_rehash($hashedPassword, $this->opsLimit, $this->memLimit); + } + + throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.'); + } +} diff --git a/lib/symfony/password-hasher/Hasher/UserPasswordHasher.php b/lib/symfony/password-hasher/Hasher/UserPasswordHasher.php new file mode 100644 index 0000000000..733a1c4e1b --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/UserPasswordHasher.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +/** + * Hashes passwords based on the user and the PasswordHasherFactory. + * + * @author Ariel Ferrandini + * + * @final + */ +class UserPasswordHasher implements UserPasswordHasherInterface +{ + private PasswordHasherFactoryInterface $hasherFactory; + + public function __construct(PasswordHasherFactoryInterface $hasherFactory) + { + $this->hasherFactory = $hasherFactory; + } + + public function hashPassword(PasswordAuthenticatedUserInterface $user, #[\SensitiveParameter] string $plainPassword): string + { + $salt = null; + if ($user instanceof LegacyPasswordAuthenticatedUserInterface) { + $salt = $user->getSalt(); + } + + $hasher = $this->hasherFactory->getPasswordHasher($user); + + return $hasher->hash($plainPassword, $salt); + } + + public function isPasswordValid(PasswordAuthenticatedUserInterface $user, #[\SensitiveParameter] string $plainPassword): bool + { + $salt = null; + if ($user instanceof LegacyPasswordAuthenticatedUserInterface) { + $salt = $user->getSalt(); + } + + if (null === $user->getPassword()) { + return false; + } + + $hasher = $this->hasherFactory->getPasswordHasher($user); + + return $hasher->verify($user->getPassword(), $plainPassword, $salt); + } + + public function needsRehash(PasswordAuthenticatedUserInterface $user): bool + { + if (null === $user->getPassword()) { + return false; + } + + $hasher = $this->hasherFactory->getPasswordHasher($user); + + return $hasher->needsRehash($user->getPassword()); + } +} diff --git a/lib/symfony/password-hasher/Hasher/UserPasswordHasherInterface.php b/lib/symfony/password-hasher/Hasher/UserPasswordHasherInterface.php new file mode 100644 index 0000000000..8d4cc1e342 --- /dev/null +++ b/lib/symfony/password-hasher/Hasher/UserPasswordHasherInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher\Hasher; + +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +/** + * Interface for the user password hasher service. + * + * @author Ariel Ferrandini + */ +interface UserPasswordHasherInterface +{ + /** + * Hashes the plain password for the given user. + */ + public function hashPassword(PasswordAuthenticatedUserInterface $user, #[\SensitiveParameter] string $plainPassword): string; + + /** + * Checks if the plaintext password matches the user's password. + */ + public function isPasswordValid(PasswordAuthenticatedUserInterface $user, #[\SensitiveParameter] string $plainPassword): bool; + + /** + * Checks if an encoded password would benefit from rehashing. + */ + public function needsRehash(PasswordAuthenticatedUserInterface $user): bool; +} diff --git a/lib/symfony/password-hasher/LICENSE b/lib/symfony/password-hasher/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/password-hasher/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/password-hasher/LegacyPasswordHasherInterface.php b/lib/symfony/password-hasher/LegacyPasswordHasherInterface.php new file mode 100644 index 0000000000..8efef376b1 --- /dev/null +++ b/lib/symfony/password-hasher/LegacyPasswordHasherInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PasswordHasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; + +/** + * Provides password hashing and verification capabilities for "legacy" hashers that require external salts. + * + * @author Fabien Potencier + * @author Nicolas Grekas + * @author Robin Chalas + */ +interface LegacyPasswordHasherInterface extends PasswordHasherInterface +{ + /** + * Hashes a plain password. + * + * @throws InvalidPasswordException If the plain password is invalid, e.g. excessively long + */ + public function hash(#[\SensitiveParameter] string $plainPassword, ?string $salt = null): string; + + /** + * Checks that a plain password and a salt match a password hash. + */ + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword, ?string $salt = null): bool; +} diff --git a/lib/symfony/password-hasher/PasswordHasherInterface.php b/lib/symfony/password-hasher/PasswordHasherInterface.php new file mode 100644 index 0000000000..6e09db3d10 --- /dev/null +++ b/lib/symfony/password-hasher/PasswordHasherInterface.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\PasswordHasher; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; + +/** + * Provides password hashing capabilities. + * + * @author Robin Chalas + * @author Fabien Potencier + * @author Nicolas Grekas + */ +interface PasswordHasherInterface +{ + public const MAX_PASSWORD_LENGTH = 4096; + + /** + * Hashes a plain password. + * + * @throws InvalidPasswordException When the plain password is invalid, e.g. excessively long + */ + public function hash(#[\SensitiveParameter] string $plainPassword): string; + + /** + * Verifies a plain password against a hash. + */ + public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword): bool; + + /** + * Checks if a password hash would benefit from rehashing. + */ + public function needsRehash(string $hashedPassword): bool; +} diff --git a/lib/symfony/password-hasher/README.md b/lib/symfony/password-hasher/README.md new file mode 100644 index 0000000000..0878746fca --- /dev/null +++ b/lib/symfony/password-hasher/README.md @@ -0,0 +1,40 @@ +PasswordHasher Component +======================== + +The PasswordHasher component provides secure password hashing utilities. + +Getting Started +--------------- + +``` +$ composer require symfony/password-hasher +``` + +```php +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; + +// Configure different password hashers via the factory +$factory = new PasswordHasherFactory([ + 'common' => ['algorithm' => 'bcrypt'], + 'memory-hard' => ['algorithm' => 'sodium'], +]); + +// Retrieve the right password hasher by its name +$passwordHasher = $factory->getPasswordHasher('common'); + +// Hash a plain password +$hash = $passwordHasher->hash('plain'); // returns a bcrypt hash + +// Verify that a given plain password matches the hash +$passwordHasher->verify($hash, 'wrong'); // returns false +$passwordHasher->verify($hash, 'plain'); // returns true (valid) +``` + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/security.html#c-hashing-passwords) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/lib/symfony/password-hasher/composer.json b/lib/symfony/password-hasher/composer.json new file mode 100644 index 0000000000..3acfde9c9c --- /dev/null +++ b/lib/symfony/password-hasher/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/password-hasher", + "type": "library", + "description": "Provides password hashing utilities", + "keywords": ["password", "hashing"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\PasswordHasher\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/lib/symfony/security-core/Authentication/AuthenticationTrustResolver.php b/lib/symfony/security-core/Authentication/AuthenticationTrustResolver.php new file mode 100644 index 0000000000..513f0d5e76 --- /dev/null +++ b/lib/symfony/security-core/Authentication/AuthenticationTrustResolver.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication; + +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * The default implementation of the authentication trust resolver. + * + * @author Johannes M. Schmitt + */ +class AuthenticationTrustResolver implements AuthenticationTrustResolverInterface +{ + public function isAuthenticated(?TokenInterface $token = null): bool + { + return $token && $token->getUser(); + } + + public function isRememberMe(?TokenInterface $token = null): bool + { + return $token && $token instanceof RememberMeToken; + } + + public function isFullFledged(?TokenInterface $token = null): bool + { + return $this->isAuthenticated($token) && !$this->isRememberMe($token); + } +} diff --git a/lib/symfony/security-core/Authentication/AuthenticationTrustResolverInterface.php b/lib/symfony/security-core/Authentication/AuthenticationTrustResolverInterface.php new file mode 100644 index 0000000000..b508290ccd --- /dev/null +++ b/lib/symfony/security-core/Authentication/AuthenticationTrustResolverInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Interface for resolving the authentication status of a given token. + * + * @author Johannes M. Schmitt + */ +interface AuthenticationTrustResolverInterface +{ + /** + * Resolves whether the passed token implementation is authenticated. + */ + public function isAuthenticated(?TokenInterface $token = null): bool; + + /** + * Resolves whether the passed token implementation is authenticated + * using remember-me capabilities. + */ + public function isRememberMe(?TokenInterface $token = null): bool; + + /** + * Resolves whether the passed token implementation is fully authenticated. + */ + public function isFullFledged(?TokenInterface $token = null): bool; +} diff --git a/lib/symfony/security-core/Authentication/RememberMe/CacheTokenVerifier.php b/lib/symfony/security-core/Authentication/RememberMe/CacheTokenVerifier.php new file mode 100644 index 0000000000..e4f1362a16 --- /dev/null +++ b/lib/symfony/security-core/Authentication/RememberMe/CacheTokenVerifier.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * @author Jordi Boggiano + */ +class CacheTokenVerifier implements TokenVerifierInterface +{ + private CacheItemPoolInterface $cache; + private int $outdatedTokenTtl; + private string $cacheKeyPrefix; + + /** + * @param int $outdatedTokenTtl How long the outdated token should still be considered valid. Defaults + * to 60, which matches how often the PersistentRememberMeHandler will at + * most refresh tokens. Increasing to more than that is not recommended, + * but you may use a lower value. + */ + public function __construct(CacheItemPoolInterface $cache, int $outdatedTokenTtl = 60, string $cacheKeyPrefix = 'rememberme-stale-') + { + $this->cache = $cache; + $this->outdatedTokenTtl = $outdatedTokenTtl; + $this->cacheKeyPrefix = $cacheKeyPrefix; + } + + public function verifyToken(PersistentTokenInterface $token, #[\SensitiveParameter] string $tokenValue): bool + { + if (hash_equals($token->getTokenValue(), $tokenValue)) { + return true; + } + + $cacheKey = $this->getCacheKey($token); + $item = $this->cache->getItem($cacheKey); + if (!$item->isHit()) { + return false; + } + + $outdatedToken = $item->get(); + + return hash_equals($outdatedToken, $tokenValue); + } + + public function updateExistingToken(PersistentTokenInterface $token, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed): void + { + // When a token gets updated, persist the outdated token for $outdatedTokenTtl seconds so we can + // still accept it as valid in verifyToken + $item = $this->cache->getItem($this->getCacheKey($token)); + $item->set($token->getTokenValue()); + $item->expiresAfter($this->outdatedTokenTtl); + $this->cache->save($item); + } + + private function getCacheKey(PersistentTokenInterface $token): string + { + return $this->cacheKeyPrefix.rawurlencode($token->getSeries()); + } +} diff --git a/lib/symfony/security-core/Authentication/RememberMe/InMemoryTokenProvider.php b/lib/symfony/security-core/Authentication/RememberMe/InMemoryTokenProvider.php new file mode 100644 index 0000000000..d2e39daf68 --- /dev/null +++ b/lib/symfony/security-core/Authentication/RememberMe/InMemoryTokenProvider.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +use Symfony\Component\Security\Core\Exception\TokenNotFoundException; + +/** + * This class is used for testing purposes, and is not really suited for production. + * + * @author Johannes M. Schmitt + * + * @final since Symfony 6.4 + */ +class InMemoryTokenProvider implements TokenProviderInterface +{ + private array $tokens = []; + + public function loadTokenBySeries(string $series): PersistentTokenInterface + { + if (!isset($this->tokens[$series])) { + throw new TokenNotFoundException('No token found.'); + } + + return $this->tokens[$series]; + } + + /** + * @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.4 + * + * @return void + */ + public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed) + { + if (!isset($this->tokens[$series])) { + throw new TokenNotFoundException('No token found.'); + } + + $token = new PersistentToken( + $this->tokens[$series]->getClass(), + $this->tokens[$series]->getUserIdentifier(), + $series, + $tokenValue, + $lastUsed + ); + $this->tokens[$series] = $token; + } + + /** + * @return void + */ + public function deleteTokenBySeries(string $series) + { + unset($this->tokens[$series]); + } + + /** + * @return void + */ + public function createNewToken(PersistentTokenInterface $token) + { + $this->tokens[$token->getSeries()] = $token; + } +} diff --git a/lib/symfony/security-core/Authentication/RememberMe/PersistentToken.php b/lib/symfony/security-core/Authentication/RememberMe/PersistentToken.php new file mode 100644 index 0000000000..f473ccb7de --- /dev/null +++ b/lib/symfony/security-core/Authentication/RememberMe/PersistentToken.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +/** + * @author Johannes M. Schmitt + * + * @internal + */ +final class PersistentToken implements PersistentTokenInterface +{ + private string $class; + private string $userIdentifier; + private string $series; + private string $tokenValue; + private \DateTimeImmutable $lastUsed; + + public function __construct(string $class, string $userIdentifier, string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed) + { + if (empty($class)) { + throw new \InvalidArgumentException('$class must not be empty.'); + } + if ('' === $userIdentifier) { + throw new \InvalidArgumentException('$userIdentifier must not be empty.'); + } + if (empty($series)) { + throw new \InvalidArgumentException('$series must not be empty.'); + } + if (empty($tokenValue)) { + throw new \InvalidArgumentException('$tokenValue must not be empty.'); + } + + $this->class = $class; + $this->userIdentifier = $userIdentifier; + $this->series = $series; + $this->tokenValue = $tokenValue; + $this->lastUsed = \DateTimeImmutable::createFromInterface($lastUsed); + } + + public function getClass(): string + { + return $this->class; + } + + public function getUserIdentifier(): string + { + return $this->userIdentifier; + } + + public function getSeries(): string + { + return $this->series; + } + + public function getTokenValue(): string + { + return $this->tokenValue; + } + + public function getLastUsed(): \DateTime + { + return \DateTime::createFromImmutable($this->lastUsed); + } +} diff --git a/lib/symfony/security-core/Authentication/RememberMe/PersistentTokenInterface.php b/lib/symfony/security-core/Authentication/RememberMe/PersistentTokenInterface.php new file mode 100644 index 0000000000..f5c0617522 --- /dev/null +++ b/lib/symfony/security-core/Authentication/RememberMe/PersistentTokenInterface.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +/** + * Interface to be implemented by persistent token classes (such as + * Doctrine entities representing a remember-me token). + * + * @author Johannes M. Schmitt + */ +interface PersistentTokenInterface +{ + /** + * Returns the class of the user. + */ + public function getClass(): string; + + /** + * Returns the series. + */ + public function getSeries(): string; + + /** + * Returns the token value. + */ + public function getTokenValue(): string; + + /** + * Returns the time the token was last used. + * + * Each call SHOULD return a new distinct DateTime instance. + */ + public function getLastUsed(): \DateTime; + + /** + * Returns the identifier used to authenticate (e.g. their email address or username). + */ + public function getUserIdentifier(): string; +} diff --git a/lib/symfony/security-core/Authentication/RememberMe/TokenProviderInterface.php b/lib/symfony/security-core/Authentication/RememberMe/TokenProviderInterface.php new file mode 100644 index 0000000000..d2f0c8cbe0 --- /dev/null +++ b/lib/symfony/security-core/Authentication/RememberMe/TokenProviderInterface.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +use Symfony\Component\Security\Core\Exception\TokenNotFoundException; + +/** + * Interface for TokenProviders. + * + * @author Johannes M. Schmitt + */ +interface TokenProviderInterface +{ + /** + * Loads the active token for the given series. + * + * @return PersistentTokenInterface + * + * @throws TokenNotFoundException if the token is not found + */ + public function loadTokenBySeries(string $series); + + /** + * Deletes all tokens belonging to series. + * + * @return void + */ + public function deleteTokenBySeries(string $series); + + /** + * Updates the token according to this data. + * + * @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.4 + * + * @return void + * + * @throws TokenNotFoundException if the token is not found + */ + public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed); + + /** + * Creates a new token. + * + * @return void + */ + public function createNewToken(PersistentTokenInterface $token); +} diff --git a/lib/symfony/security-core/Authentication/RememberMe/TokenVerifierInterface.php b/lib/symfony/security-core/Authentication/RememberMe/TokenVerifierInterface.php new file mode 100644 index 0000000000..a323175073 --- /dev/null +++ b/lib/symfony/security-core/Authentication/RememberMe/TokenVerifierInterface.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\Security\Core\Authentication\RememberMe; + +/** + * @author Jordi Boggiano + */ +interface TokenVerifierInterface +{ + /** + * Verifies that the given $token is valid. + * + * This lets you override the token check logic to for example accept slightly outdated tokens. + * + * Do not forget to implement token comparisons using hash_equals for a secure implementation. + */ + public function verifyToken(PersistentTokenInterface $token, #[\SensitiveParameter] string $tokenValue): bool; + + /** + * Updates an existing token with a new token value and lastUsed time. + */ + public function updateExistingToken(PersistentTokenInterface $token, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed): void; +} diff --git a/lib/symfony/security-core/Authentication/Token/AbstractToken.php b/lib/symfony/security-core/Authentication/Token/AbstractToken.php new file mode 100644 index 0000000000..d2c1763ee3 --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/AbstractToken.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Base class for Token instances. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +abstract class AbstractToken implements TokenInterface, \Serializable +{ + private ?UserInterface $user = null; + private array $roleNames = []; + private array $attributes = []; + + /** + * @param string[] $roles An array of roles + * + * @throws \InvalidArgumentException + */ + public function __construct(array $roles = []) + { + foreach ($roles as $role) { + $this->roleNames[] = $role; + } + } + + public function getRoleNames(): array + { + return $this->roleNames; + } + + public function getUserIdentifier(): string + { + return $this->user ? $this->user->getUserIdentifier() : ''; + } + + public function getUser(): ?UserInterface + { + return $this->user; + } + + /** + * @return void + */ + public function setUser(UserInterface $user) + { + $this->user = $user; + } + + /** + * @return void + */ + public function eraseCredentials() + { + if ($this->getUser() instanceof UserInterface) { + $this->getUser()->eraseCredentials(); + } + } + + /** + * Returns all the necessary state of the object for serialization purposes. + * + * There is no need to serialize any entry, they should be returned as-is. + * If you extend this method, keep in mind you MUST guarantee parent data is present in the state. + * Here is an example of how to extend this method: + * + * public function __serialize(): array + * { + * return [$this->childAttribute, parent::__serialize()]; + * } + * + * + * @see __unserialize() + */ + public function __serialize(): array + { + return [$this->user, true, null, $this->attributes, $this->roleNames]; + } + + /** + * Restores the object state from an array given by __serialize(). + * + * There is no need to unserialize any entry in $data, they are already ready-to-use. + * If you extend this method, keep in mind you MUST pass the parent data to its respective class. + * Here is an example of how to extend this method: + * + * public function __unserialize(array $data): void + * { + * [$this->childAttribute, $parentData] = $data; + * parent::__unserialize($parentData); + * } + * + * + * @see __serialize() + */ + public function __unserialize(array $data): void + { + [$user, , , $this->attributes, $this->roleNames] = $data; + $this->user = \is_string($user) ? new InMemoryUser($user, '', $this->roleNames, false) : $user; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @return void + */ + public function setAttributes(array $attributes) + { + $this->attributes = $attributes; + } + + public function hasAttribute(string $name): bool + { + return \array_key_exists($name, $this->attributes); + } + + public function getAttribute(string $name): mixed + { + if (!\array_key_exists($name, $this->attributes)) { + throw new \InvalidArgumentException(\sprintf('This token has no "%s" attribute.', $name)); + } + + return $this->attributes[$name]; + } + + /** + * @return void + */ + public function setAttribute(string $name, mixed $value) + { + $this->attributes[$name] = $value; + } + + public function __toString(): string + { + $class = static::class; + $class = substr($class, strrpos($class, '\\') + 1); + + $roles = []; + foreach ($this->roleNames as $role) { + $roles[] = $role; + } + + return \sprintf('%s(user="%s", roles="%s")', $class, $this->getUserIdentifier(), implode(', ', $roles)); + } + + /** + * @internal + */ + final public function serialize(): string + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + /** + * @internal + */ + final public function unserialize(string $serialized): void + { + $this->__unserialize(unserialize($serialized)); + } +} diff --git a/lib/symfony/security-core/Authentication/Token/NullToken.php b/lib/symfony/security-core/Authentication/Token/NullToken.php new file mode 100644 index 0000000000..eabfe17bba --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/NullToken.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * @author Wouter de Jong + */ +class NullToken implements TokenInterface +{ + public function __toString(): string + { + return ''; + } + + public function getRoleNames(): array + { + return []; + } + + public function getUser(): ?UserInterface + { + return null; + } + + /** + * @return never + */ + public function setUser(UserInterface $user) + { + throw new \BadMethodCallException('Cannot set user on a NullToken.'); + } + + public function getUserIdentifier(): string + { + return ''; + } + + /** + * @return void + */ + public function eraseCredentials() + { + } + + public function getAttributes(): array + { + return []; + } + + /** + * @return never + */ + public function setAttributes(array $attributes) + { + throw new \BadMethodCallException('Cannot set attributes of NullToken.'); + } + + public function hasAttribute(string $name): bool + { + return false; + } + + public function getAttribute(string $name): mixed + { + return null; + } + + /** + * @return never + */ + public function setAttribute(string $name, mixed $value) + { + throw new \BadMethodCallException('Cannot add attribute to NullToken.'); + } + + public function __serialize(): array + { + return []; + } + + public function __unserialize(array $data): void + { + } +} diff --git a/lib/symfony/security-core/Authentication/Token/PreAuthenticatedToken.php b/lib/symfony/security-core/Authentication/Token/PreAuthenticatedToken.php new file mode 100644 index 0000000000..a216d4c180 --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/PreAuthenticatedToken.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * PreAuthenticatedToken implements a pre-authenticated token. + * + * @author Fabien Potencier + */ +class PreAuthenticatedToken extends AbstractToken +{ + private string $firewallName; + + /** + * @param string[] $roles + */ + public function __construct(UserInterface $user, string $firewallName, array $roles = []) + { + parent::__construct($roles); + + if ('' === $firewallName) { + throw new \InvalidArgumentException('$firewallName must not be empty.'); + } + + $this->setUser($user); + $this->firewallName = $firewallName; + } + + public function getFirewallName(): string + { + return $this->firewallName; + } + + public function __serialize(): array + { + return [null, $this->firewallName, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [, $this->firewallName, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Authentication/Token/RememberMeToken.php b/lib/symfony/security-core/Authentication/Token/RememberMeToken.php new file mode 100644 index 0000000000..ad218f1b3d --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/RememberMeToken.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Authentication Token for "Remember-Me". + * + * @author Johannes M. Schmitt + */ +class RememberMeToken extends AbstractToken +{ + private string $secret; + private string $firewallName; + + /** + * @param string $secret A secret used to make sure the token is created by the app and not by a malicious client + * + * @throws \InvalidArgumentException + */ + public function __construct(UserInterface $user, string $firewallName, #[\SensitiveParameter] string $secret) + { + parent::__construct($user->getRoles()); + + if (!$secret) { + throw new InvalidArgumentException('A non-empty secret is required.'); + } + + if (!$firewallName) { + throw new InvalidArgumentException('$firewallName must not be empty.'); + } + + $this->firewallName = $firewallName; + $this->secret = $secret; + + $this->setUser($user); + } + + public function getFirewallName(): string + { + return $this->firewallName; + } + + public function getSecret(): string + { + return $this->secret; + } + + public function __serialize(): array + { + return [$this->secret, $this->firewallName, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->secret, $this->firewallName, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Authentication/Token/Storage/TokenStorage.php b/lib/symfony/security-core/Authentication/Token/Storage/TokenStorage.php new file mode 100644 index 0000000000..52a945a9ec --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/Storage/TokenStorage.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token\Storage; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * TokenStorage contains a TokenInterface. + * + * It gives access to the token representing the current user authentication. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class TokenStorage implements TokenStorageInterface, ResetInterface +{ + private ?TokenInterface $token = null; + private ?\Closure $initializer = null; + + public function getToken(): ?TokenInterface + { + if ($initializer = $this->initializer) { + $this->initializer = null; + $initializer(); + } + + return $this->token; + } + + /** + * @return void + */ + public function setToken(?TokenInterface $token = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/security-core', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + + if ($token) { + // ensure any initializer is called + $this->getToken(); + } + + $this->initializer = null; + $this->token = $token; + } + + public function setInitializer(?callable $initializer): void + { + $this->initializer = null === $initializer ? null : $initializer(...); + } + + /** + * @return void + */ + public function reset() + { + $this->setToken(null); + } +} diff --git a/lib/symfony/security-core/Authentication/Token/Storage/TokenStorageInterface.php b/lib/symfony/security-core/Authentication/Token/Storage/TokenStorageInterface.php new file mode 100644 index 0000000000..5fdfa4e9ff --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/Storage/TokenStorageInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token\Storage; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * The TokenStorageInterface. + * + * @author Johannes M. Schmitt + */ +interface TokenStorageInterface +{ + /** + * Returns the current security token. + */ + public function getToken(): ?TokenInterface; + + /** + * Sets the authentication token. + * + * @param TokenInterface|null $token A TokenInterface token, or null if no further authentication information should be stored + * + * @return void + */ + public function setToken(?TokenInterface $token); +} diff --git a/lib/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php b/lib/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php new file mode 100644 index 0000000000..8a4069e7ed --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token\Storage; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; + +/** + * A token storage that increments the session usage index when the token is accessed. + * + * @author Nicolas Grekas + */ +final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceSubscriberInterface +{ + private TokenStorageInterface $storage; + private ContainerInterface $container; + private bool $enableUsageTracking = false; + + public function __construct(TokenStorageInterface $storage, ContainerInterface $container) + { + $this->storage = $storage; + $this->container = $container; + } + + public function getToken(): ?TokenInterface + { + if ($this->shouldTrackUsage()) { + // increments the internal session usage index + $this->getSession()->getMetadataBag(); + } + + return $this->storage->getToken(); + } + + public function setToken(?TokenInterface $token = null): void + { + $this->storage->setToken($token); + + if ($token && $this->shouldTrackUsage()) { + // increments the internal session usage index + $this->getSession()->getMetadataBag(); + } + } + + public function enableUsageTracking(): void + { + $this->enableUsageTracking = true; + } + + public function disableUsageTracking(): void + { + $this->enableUsageTracking = false; + } + + public static function getSubscribedServices(): array + { + return [ + 'request_stack' => RequestStack::class, + ]; + } + + private function getSession(): SessionInterface + { + return $this->container->get('request_stack')->getSession(); + } + + private function shouldTrackUsage(): bool + { + return $this->enableUsageTracking && $this->container->get('request_stack')->getMainRequest(); + } +} diff --git a/lib/symfony/security-core/Authentication/Token/SwitchUserToken.php b/lib/symfony/security-core/Authentication/Token/SwitchUserToken.php new file mode 100644 index 0000000000..fb632a616d --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/SwitchUserToken.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Token representing a user who temporarily impersonates another one. + * + * @author Christian Flothmann + */ +class SwitchUserToken extends UsernamePasswordToken +{ + private TokenInterface $originalToken; + private ?string $originatedFromUri = null; + + /** + * @param $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method + * @param $originatedFromUri The URI where was the user at the switch + * + * @throws \InvalidArgumentException + */ + public function __construct(UserInterface $user, string $firewallName, array $roles, TokenInterface $originalToken, ?string $originatedFromUri = null) + { + parent::__construct($user, $firewallName, $roles); + + $this->originalToken = $originalToken; + $this->originatedFromUri = $originatedFromUri; + } + + public function getOriginalToken(): TokenInterface + { + return $this->originalToken; + } + + public function getOriginatedFromUri(): ?string + { + return $this->originatedFromUri; + } + + public function __serialize(): array + { + return [$this->originalToken, $this->originatedFromUri, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + if (3 > \count($data)) { + // Support for tokens serialized with version 5.1 or lower of symfony/security-core. + [$this->originalToken, $parentData] = $data; + } else { + [$this->originalToken, $this->originatedFromUri, $parentData] = $data; + } + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Authentication/Token/TokenInterface.php b/lib/symfony/security-core/Authentication/Token/TokenInterface.php new file mode 100644 index 0000000000..7d8800565a --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/TokenInterface.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\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * TokenInterface is the interface for the user authentication information. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +interface TokenInterface extends \Stringable +{ + /** + * Returns a string representation of the Token. + * + * This is only to be used for debugging purposes. + */ + public function __toString(): string; + + /** + * Returns the user identifier used during authentication (e.g. a user's email address or username). + */ + public function getUserIdentifier(): string; + + /** + * Returns the user roles. + * + * @return string[] + */ + public function getRoleNames(): array; + + /** + * Returns a user representation. + * + * @see AbstractToken::setUser() + */ + public function getUser(): ?UserInterface; + + /** + * Sets the authenticated user in the token. + * + * @return void + * + * @throws \InvalidArgumentException + */ + public function setUser(UserInterface $user); + + /** + * Removes sensitive information from the token. + * + * @return void + */ + public function eraseCredentials(); + + public function getAttributes(): array; + + /** + * @param array $attributes The token attributes + * + * @return void + */ + public function setAttributes(array $attributes); + + public function hasAttribute(string $name): bool; + + /** + * @throws \InvalidArgumentException When attribute doesn't exist for this token + */ + public function getAttribute(string $name): mixed; + + /** + * @return void + */ + public function setAttribute(string $name, mixed $value); + + /** + * Returns all the necessary state of the object for serialization purposes. + */ + public function __serialize(): array; + + /** + * Restores the object state from an array given by __serialize(). + */ + public function __unserialize(array $data): void; +} diff --git a/lib/symfony/security-core/Authentication/Token/UsernamePasswordToken.php b/lib/symfony/security-core/Authentication/Token/UsernamePasswordToken.php new file mode 100644 index 0000000000..74e24a2115 --- /dev/null +++ b/lib/symfony/security-core/Authentication/Token/UsernamePasswordToken.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * UsernamePasswordToken implements a username and password token. + * + * @author Fabien Potencier + */ +class UsernamePasswordToken extends AbstractToken +{ + private string $firewallName; + + public function __construct(UserInterface $user, string $firewallName, array $roles = []) + { + parent::__construct($roles); + + if ('' === $firewallName) { + throw new \InvalidArgumentException('$firewallName must not be empty.'); + } + + $this->setUser($user); + $this->firewallName = $firewallName; + } + + public function getFirewallName(): string + { + return $this->firewallName; + } + + public function __serialize(): array + { + return [null, $this->firewallName, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [, $this->firewallName, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/AuthenticationEvents.php b/lib/symfony/security-core/AuthenticationEvents.php new file mode 100644 index 0000000000..a1c3e5dd0b --- /dev/null +++ b/lib/symfony/security-core/AuthenticationEvents.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\Security\Core; + +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; + +final class AuthenticationEvents +{ + /** + * The AUTHENTICATION_SUCCESS event occurs after a user is authenticated + * by one provider. + * + * @Event("Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent") + */ + public const AUTHENTICATION_SUCCESS = 'security.authentication.success'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + public const ALIASES = [ + AuthenticationSuccessEvent::class => self::AUTHENTICATION_SUCCESS, + ]; +} diff --git a/lib/symfony/security-core/Authorization/AccessDecisionManager.php b/lib/symfony/security-core/Authorization/AccessDecisionManager.php new file mode 100644 index 0000000000..009a046367 --- /dev/null +++ b/lib/symfony/security-core/Authorization/AccessDecisionManager.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface; +use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; +use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; + +/** + * AccessDecisionManager is the base class for all access decision managers + * that use decision voters. + * + * @author Fabien Potencier + */ +final class AccessDecisionManager implements AccessDecisionManagerInterface +{ + private const VALID_VOTES = [ + VoterInterface::ACCESS_GRANTED => true, + VoterInterface::ACCESS_DENIED => true, + VoterInterface::ACCESS_ABSTAIN => true, + ]; + + private iterable $voters; + private array $votersCacheAttributes = []; + private array $votersCacheObject = []; + private AccessDecisionStrategyInterface $strategy; + + /** + * @param iterable $voters An array or an iterator of VoterInterface instances + */ + public function __construct(iterable $voters = [], ?AccessDecisionStrategyInterface $strategy = null) + { + $this->voters = $voters; + $this->strategy = $strategy ?? new AffirmativeStrategy(); + } + + /** + * @param bool $allowMultipleAttributes Whether to allow passing multiple values to the $attributes array + */ + public function decide(TokenInterface $token, array $attributes, mixed $object = null, bool $allowMultipleAttributes = false): bool + { + // Special case for AccessListener, do not remove the right side of the condition before 6.0 + if (\count($attributes) > 1 && !$allowMultipleAttributes) { + throw new InvalidArgumentException(\sprintf('Passing more than one Security attribute to "%s()" is not supported.', __METHOD__)); + } + + return $this->strategy->decide( + $this->collectResults($token, $attributes, $object) + ); + } + + /** + * @return \Traversable + */ + private function collectResults(TokenInterface $token, array $attributes, mixed $object): \Traversable + { + foreach ($this->getVoters($attributes, $object) as $voter) { + $result = $voter->vote($token, $object, $attributes); + if (!\is_int($result) || !(self::VALID_VOTES[$result] ?? false)) { + throw new \LogicException(\sprintf('"%s::vote()" must return one of "%s" constants ("ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN"), "%s" returned.', get_debug_type($voter), VoterInterface::class, var_export($result, true))); + } + + yield $result; + } + } + + /** + * @return iterable + */ + private function getVoters(array $attributes, $object = null): iterable + { + $keyAttributes = []; + foreach ($attributes as $attribute) { + $keyAttributes[] = \is_string($attribute) ? $attribute : null; + } + // use `get_class` to handle anonymous classes + $keyObject = \is_object($object) ? $object::class : get_debug_type($object); + foreach ($this->voters as $key => $voter) { + if (!$voter instanceof CacheableVoterInterface) { + yield $voter; + continue; + } + + $supports = true; + // The voter supports the attributes if it supports at least one attribute of the list + foreach ($keyAttributes as $keyAttribute) { + if (null === $keyAttribute) { + $supports = true; + } elseif (!isset($this->votersCacheAttributes[$keyAttribute][$key])) { + $this->votersCacheAttributes[$keyAttribute][$key] = $supports = $voter->supportsAttribute($keyAttribute); + } else { + $supports = $this->votersCacheAttributes[$keyAttribute][$key]; + } + if ($supports) { + break; + } + } + if (!$supports) { + continue; + } + + if (!isset($this->votersCacheObject[$keyObject][$key])) { + $this->votersCacheObject[$keyObject][$key] = $supports = $voter->supportsType($keyObject); + } else { + $supports = $this->votersCacheObject[$keyObject][$key]; + } + if (!$supports) { + continue; + } + yield $voter; + } + } +} diff --git a/lib/symfony/security-core/Authorization/AccessDecisionManagerInterface.php b/lib/symfony/security-core/Authorization/AccessDecisionManagerInterface.php new file mode 100644 index 0000000000..f25c7e1bef --- /dev/null +++ b/lib/symfony/security-core/Authorization/AccessDecisionManagerInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * AccessDecisionManagerInterface makes authorization decisions. + * + * @author Fabien Potencier + */ +interface AccessDecisionManagerInterface +{ + /** + * Decides whether the access is possible or not. + * + * @param array $attributes An array of attributes associated with the method being invoked + * @param mixed $object The object to secure + */ + public function decide(TokenInterface $token, array $attributes, mixed $object = null): bool; +} diff --git a/lib/symfony/security-core/Authorization/AuthorizationChecker.php b/lib/symfony/security-core/Authorization/AuthorizationChecker.php new file mode 100644 index 0000000000..08ad3b0525 --- /dev/null +++ b/lib/symfony/security-core/Authorization/AuthorizationChecker.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Symfony\Component\Security\Core\Authentication\Token\NullToken; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +/** + * AuthorizationChecker is the main authorization point of the Security component. + * + * It gives access to the token representing the current user authentication. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class AuthorizationChecker implements AuthorizationCheckerInterface +{ + private TokenStorageInterface $tokenStorage; + private AccessDecisionManagerInterface $accessDecisionManager; + + public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, bool $exceptionOnNoToken = false) + { + if ($exceptionOnNoToken) { + throw new \LogicException(\sprintf('Argument $exceptionOnNoToken of "%s()" must be set to "false".', __METHOD__)); + } + + $this->tokenStorage = $tokenStorage; + $this->accessDecisionManager = $accessDecisionManager; + } + + final public function isGranted(mixed $attribute, mixed $subject = null): bool + { + $token = $this->tokenStorage->getToken(); + + if (!$token || !$token->getUser()) { + $token = new NullToken(); + } + + return $this->accessDecisionManager->decide($token, [$attribute], $subject); + } +} diff --git a/lib/symfony/security-core/Authorization/AuthorizationCheckerInterface.php b/lib/symfony/security-core/Authorization/AuthorizationCheckerInterface.php new file mode 100644 index 0000000000..6f5a602217 --- /dev/null +++ b/lib/symfony/security-core/Authorization/AuthorizationCheckerInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +/** + * The AuthorizationCheckerInterface. + * + * @author Johannes M. Schmitt + */ +interface AuthorizationCheckerInterface +{ + /** + * Checks if the attribute is granted against the current authentication token and optionally supplied subject. + * + * @param mixed $attribute A single attribute to vote on (can be of any type, string and instance of Expression are supported by the core) + */ + public function isGranted(mixed $attribute, mixed $subject = null): bool; +} diff --git a/lib/symfony/security-core/Authorization/ExpressionLanguage.php b/lib/symfony/security-core/Authorization/ExpressionLanguage.php new file mode 100644 index 0000000000..846d2cf651 --- /dev/null +++ b/lib/symfony/security-core/Authorization/ExpressionLanguage.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; + +if (!class_exists(BaseExpressionLanguage::class)) { + throw new \LogicException(\sprintf('The "%s" class requires the "ExpressionLanguage" component. Try running "composer require symfony/expression-language".', ExpressionLanguage::class)); +} else { + // Help opcache.preload discover always-needed symbols + class_exists(ExpressionLanguageProvider::class); + + /** + * Adds some function to the default ExpressionLanguage. + * + * @author Fabien Potencier + * + * @see ExpressionLanguageProvider + */ + class ExpressionLanguage extends BaseExpressionLanguage + { + public function __construct(?CacheItemPoolInterface $cache = null, array $providers = []) + { + // prepend the default provider to let users override it easily + array_unshift($providers, new ExpressionLanguageProvider()); + + parent::__construct($cache, $providers); + } + } +} diff --git a/lib/symfony/security-core/Authorization/ExpressionLanguageProvider.php b/lib/symfony/security-core/Authorization/ExpressionLanguageProvider.php new file mode 100644 index 0000000000..2e558c214e --- /dev/null +++ b/lib/symfony/security-core/Authorization/ExpressionLanguageProvider.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + +/** + * Define some ExpressionLanguage functions. + * + * @author Fabien Potencier + */ +class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface +{ + public function getFunctions(): array + { + return [ + new ExpressionFunction('is_authenticated', fn () => '$auth_checker->isGranted("IS_AUTHENTICATED")', fn (array $variables) => $variables['auth_checker']->isGranted('IS_AUTHENTICATED')), + + new ExpressionFunction('is_fully_authenticated', fn () => '$token && $auth_checker->isGranted("IS_AUTHENTICATED_FULLY")', fn (array $variables) => $variables['token'] && $variables['auth_checker']->isGranted('IS_AUTHENTICATED_FULLY')), + + new ExpressionFunction('is_granted', fn ($attributes, $object = 'null') => \sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object), fn (array $variables, $attributes, $object = null) => $variables['auth_checker']->isGranted($attributes, $object)), + + new ExpressionFunction('is_remember_me', fn () => '$token && $auth_checker->isGranted("IS_REMEMBERED")', fn (array $variables) => $variables['token'] && $variables['auth_checker']->isGranted('IS_REMEMBERED')), + ]; + } +} diff --git a/lib/symfony/security-core/Authorization/Strategy/AccessDecisionStrategyInterface.php b/lib/symfony/security-core/Authorization/Strategy/AccessDecisionStrategyInterface.php new file mode 100644 index 0000000000..00238378a3 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Strategy/AccessDecisionStrategyInterface.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\Security\Core\Authorization\Strategy; + +/** + * A strategy for turning a stream of votes into a final decision. + * + * @author Alexander M. Turek + */ +interface AccessDecisionStrategyInterface +{ + /** + * @param \Traversable $results + */ + public function decide(\Traversable $results): bool; +} diff --git a/lib/symfony/security-core/Authorization/Strategy/AffirmativeStrategy.php b/lib/symfony/security-core/Authorization/Strategy/AffirmativeStrategy.php new file mode 100644 index 0000000000..ecd74b2086 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Strategy/AffirmativeStrategy.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\Security\Core\Authorization\Strategy; + +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + +/** + * Grants access if any voter returns an affirmative response. + * + * If all voters abstained from voting, the decision will be based on the + * allowIfAllAbstainDecisions property value (defaults to false). + * + * @author Fabien Potencier + * @author Alexander M. Turek + */ +final class AffirmativeStrategy implements AccessDecisionStrategyInterface, \Stringable +{ + private bool $allowIfAllAbstainDecisions; + + public function __construct(bool $allowIfAllAbstainDecisions = false) + { + $this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + public function decide(\Traversable $results): bool + { + $deny = 0; + foreach ($results as $result) { + if (VoterInterface::ACCESS_GRANTED === $result) { + return true; + } + + if (VoterInterface::ACCESS_DENIED === $result) { + ++$deny; + } + } + + if ($deny > 0) { + return false; + } + + return $this->allowIfAllAbstainDecisions; + } + + public function __toString(): string + { + return 'affirmative'; + } +} diff --git a/lib/symfony/security-core/Authorization/Strategy/ConsensusStrategy.php b/lib/symfony/security-core/Authorization/Strategy/ConsensusStrategy.php new file mode 100644 index 0000000000..489b34287b --- /dev/null +++ b/lib/symfony/security-core/Authorization/Strategy/ConsensusStrategy.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Strategy; + +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + +/** + * Grants access if there is consensus of granted against denied responses. + * + * Consensus means majority-rule (ignoring abstains) rather than unanimous + * agreement (ignoring abstains). If you require unanimity, see + * UnanimousBased. + * + * If there were an equal number of grant and deny votes, the decision will + * be based on the allowIfEqualGrantedDeniedDecisions property value + * (defaults to true). + * + * If all voters abstained from voting, the decision will be based on the + * allowIfAllAbstainDecisions property value (defaults to false). + * + * @author Fabien Potencier + * @author Alexander M. Turek + */ +final class ConsensusStrategy implements AccessDecisionStrategyInterface, \Stringable +{ + private bool $allowIfAllAbstainDecisions; + private bool $allowIfEqualGrantedDeniedDecisions; + + public function __construct(bool $allowIfAllAbstainDecisions = false, bool $allowIfEqualGrantedDeniedDecisions = true) + { + $this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + $this->allowIfEqualGrantedDeniedDecisions = $allowIfEqualGrantedDeniedDecisions; + } + + public function decide(\Traversable $results): bool + { + $grant = 0; + $deny = 0; + foreach ($results as $result) { + if (VoterInterface::ACCESS_GRANTED === $result) { + ++$grant; + } elseif (VoterInterface::ACCESS_DENIED === $result) { + ++$deny; + } + } + + if ($grant > $deny) { + return true; + } + + if ($deny > $grant) { + return false; + } + + if ($grant > 0) { + return $this->allowIfEqualGrantedDeniedDecisions; + } + + return $this->allowIfAllAbstainDecisions; + } + + public function __toString(): string + { + return 'consensus'; + } +} diff --git a/lib/symfony/security-core/Authorization/Strategy/PriorityStrategy.php b/lib/symfony/security-core/Authorization/Strategy/PriorityStrategy.php new file mode 100644 index 0000000000..9599950c7f --- /dev/null +++ b/lib/symfony/security-core/Authorization/Strategy/PriorityStrategy.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\Security\Core\Authorization\Strategy; + +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + +/** + * Grant or deny access depending on the first voter that does not abstain. + * The priority of voters can be used to overrule a decision. + * + * If all voters abstained from voting, the decision will be based on the + * allowIfAllAbstainDecisions property value (defaults to false). + * + * @author Fabien Potencier + * @author Alexander M. Turek + */ +final class PriorityStrategy implements AccessDecisionStrategyInterface, \Stringable +{ + private bool $allowIfAllAbstainDecisions; + + public function __construct(bool $allowIfAllAbstainDecisions = false) + { + $this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + public function decide(\Traversable $results): bool + { + foreach ($results as $result) { + if (VoterInterface::ACCESS_GRANTED === $result) { + return true; + } + + if (VoterInterface::ACCESS_DENIED === $result) { + return false; + } + } + + return $this->allowIfAllAbstainDecisions; + } + + public function __toString(): string + { + return 'priority'; + } +} diff --git a/lib/symfony/security-core/Authorization/Strategy/UnanimousStrategy.php b/lib/symfony/security-core/Authorization/Strategy/UnanimousStrategy.php new file mode 100644 index 0000000000..1f3b85c548 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Strategy/UnanimousStrategy.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Strategy; + +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + +/** + * Grants access if only grant (or abstain) votes were received. + * + * If all voters abstained from voting, the decision will be based on the + * allowIfAllAbstainDecisions property value (defaults to false). + * + * @author Fabien Potencier + * @author Alexander M. Turek + */ +final class UnanimousStrategy implements AccessDecisionStrategyInterface, \Stringable +{ + private bool $allowIfAllAbstainDecisions; + + public function __construct(bool $allowIfAllAbstainDecisions = false) + { + $this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + public function decide(\Traversable $results): bool + { + $grant = 0; + foreach ($results as $result) { + if (VoterInterface::ACCESS_DENIED === $result) { + return false; + } + + if (VoterInterface::ACCESS_GRANTED === $result) { + ++$grant; + } + } + + // no deny votes + if ($grant > 0) { + return true; + } + + return $this->allowIfAllAbstainDecisions; + } + + public function __toString(): string + { + return 'unanimous'; + } +} diff --git a/lib/symfony/security-core/Authorization/TraceableAccessDecisionManager.php b/lib/symfony/security-core/Authorization/TraceableAccessDecisionManager.php new file mode 100644 index 0000000000..cb44dce4c0 --- /dev/null +++ b/lib/symfony/security-core/Authorization/TraceableAccessDecisionManager.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + +/** + * Decorates the original AccessDecisionManager class to log information + * about the security voters and the decisions made by them. + * + * @author Javier Eguiluz + * + * @internal + */ +class TraceableAccessDecisionManager implements AccessDecisionManagerInterface +{ + private AccessDecisionManagerInterface $manager; + private ?AccessDecisionStrategyInterface $strategy = null; + /** @var iterable */ + private iterable $voters = []; + private array $decisionLog = []; // All decision logs + private array $currentLog = []; // Logs being filled in + + public function __construct(AccessDecisionManagerInterface $manager) + { + $this->manager = $manager; + + // The strategy and voters are stored in a private properties of the decorated service + if (property_exists($manager, 'strategy')) { + $reflection = new \ReflectionProperty($manager::class, 'strategy'); + $this->strategy = $reflection->getValue($manager); + } + if (property_exists($manager, 'voters')) { + $reflection = new \ReflectionProperty($manager::class, 'voters'); + $this->voters = $reflection->getValue($manager); + } + } + + public function decide(TokenInterface $token, array $attributes, mixed $object = null, bool $allowMultipleAttributes = false): bool + { + $currentDecisionLog = [ + 'attributes' => $attributes, + 'object' => $object, + 'voterDetails' => [], + ]; + + $this->currentLog[] = &$currentDecisionLog; + + $result = $this->manager->decide($token, $attributes, $object, $allowMultipleAttributes); + + $currentDecisionLog['result'] = $result; + + $this->decisionLog[] = array_pop($this->currentLog); // Using a stack since decide can be called by voters + + return $result; + } + + /** + * Adds voter vote and class to the voter details. + * + * @param array $attributes attributes used for the vote + * @param int $vote vote of the voter + */ + public function addVoterVote(VoterInterface $voter, array $attributes, int $vote): void + { + $currentLogIndex = \count($this->currentLog) - 1; + $this->currentLog[$currentLogIndex]['voterDetails'][] = [ + 'voter' => $voter, + 'attributes' => $attributes, + 'vote' => $vote, + ]; + } + + public function getStrategy(): string + { + if (null === $this->strategy) { + return '-'; + } + if (method_exists($this->strategy, '__toString')) { + return (string) $this->strategy; + } + + return get_debug_type($this->strategy); + } + + /** + * @return iterable + */ + public function getVoters(): iterable + { + return $this->voters; + } + + public function getDecisionLog(): array + { + return $this->decisionLog; + } +} diff --git a/lib/symfony/security-core/Authorization/Voter/AuthenticatedVoter.php b/lib/symfony/security-core/Authorization/Voter/AuthenticatedVoter.php new file mode 100644 index 0000000000..d7b2b22431 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/AuthenticatedVoter.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * AuthenticatedVoter votes if an attribute like IS_AUTHENTICATED_FULLY, + * IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED is present. + * + * This list is most restrictive to least restrictive checking. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class AuthenticatedVoter implements CacheableVoterInterface +{ + public const IS_AUTHENTICATED_FULLY = 'IS_AUTHENTICATED_FULLY'; + public const IS_AUTHENTICATED_REMEMBERED = 'IS_AUTHENTICATED_REMEMBERED'; + public const IS_AUTHENTICATED = 'IS_AUTHENTICATED'; + public const IS_IMPERSONATOR = 'IS_IMPERSONATOR'; + public const IS_REMEMBERED = 'IS_REMEMBERED'; + public const PUBLIC_ACCESS = 'PUBLIC_ACCESS'; + + private AuthenticationTrustResolverInterface $authenticationTrustResolver; + + public function __construct(AuthenticationTrustResolverInterface $authenticationTrustResolver) + { + $this->authenticationTrustResolver = $authenticationTrustResolver; + } + + public function vote(TokenInterface $token, mixed $subject, array $attributes): int + { + if ($attributes === [self::PUBLIC_ACCESS]) { + return VoterInterface::ACCESS_GRANTED; + } + + $result = VoterInterface::ACCESS_ABSTAIN; + foreach ($attributes as $attribute) { + if (null === $attribute || (self::IS_AUTHENTICATED_FULLY !== $attribute + && self::IS_AUTHENTICATED_REMEMBERED !== $attribute + && self::IS_AUTHENTICATED !== $attribute + && self::IS_IMPERSONATOR !== $attribute + && self::IS_REMEMBERED !== $attribute)) { + continue; + } + + $result = VoterInterface::ACCESS_DENIED; + + if (self::IS_AUTHENTICATED_FULLY === $attribute + && $this->authenticationTrustResolver->isFullFledged($token)) { + return VoterInterface::ACCESS_GRANTED; + } + + if (self::IS_AUTHENTICATED_REMEMBERED === $attribute + && ($this->authenticationTrustResolver->isRememberMe($token) + || $this->authenticationTrustResolver->isFullFledged($token))) { + return VoterInterface::ACCESS_GRANTED; + } + + if (self::IS_AUTHENTICATED === $attribute && $this->authenticationTrustResolver->isAuthenticated($token)) { + return VoterInterface::ACCESS_GRANTED; + } + + if (self::IS_REMEMBERED === $attribute && $this->authenticationTrustResolver->isRememberMe($token)) { + return VoterInterface::ACCESS_GRANTED; + } + + if (self::IS_IMPERSONATOR === $attribute && $token instanceof SwitchUserToken) { + return VoterInterface::ACCESS_GRANTED; + } + } + + return $result; + } + + public function supportsAttribute(string $attribute): bool + { + return \in_array($attribute, [ + self::IS_AUTHENTICATED_FULLY, + self::IS_AUTHENTICATED_REMEMBERED, + self::IS_AUTHENTICATED, + self::IS_IMPERSONATOR, + self::IS_REMEMBERED, + self::PUBLIC_ACCESS, + ], true); + } + + public function supportsType(string $subjectType): bool + { + return true; + } +} diff --git a/lib/symfony/security-core/Authorization/Voter/CacheableVoterInterface.php b/lib/symfony/security-core/Authorization/Voter/CacheableVoterInterface.php new file mode 100644 index 0000000000..875aad6601 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/CacheableVoterInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +/** + * Let voters expose the attributes and types they care about. + * + * By returning false to either `supportsAttribute` or `supportsType`, the + * voter will never be called for the specified attribute or subject. + * + * @author Jérémy Derussé + */ +interface CacheableVoterInterface extends VoterInterface +{ + public function supportsAttribute(string $attribute): bool; + + /** + * @param string $subjectType The type of the subject inferred by `get_class` or `get_debug_type` + */ + public function supportsType(string $subjectType): bool; +} diff --git a/lib/symfony/security-core/Authorization/Voter/ExpressionVoter.php b/lib/symfony/security-core/Authorization/Voter/ExpressionVoter.php new file mode 100644 index 0000000000..6de9c95465 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/ExpressionVoter.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; + +/** + * ExpressionVoter votes based on the evaluation of an expression. + * + * @author Fabien Potencier + */ +class ExpressionVoter implements CacheableVoterInterface +{ + private ExpressionLanguage $expressionLanguage; + private AuthenticationTrustResolverInterface $trustResolver; + private AuthorizationCheckerInterface $authChecker; + private ?RoleHierarchyInterface $roleHierarchy; + + public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, AuthorizationCheckerInterface $authChecker, ?RoleHierarchyInterface $roleHierarchy = null) + { + $this->expressionLanguage = $expressionLanguage; + $this->trustResolver = $trustResolver; + $this->authChecker = $authChecker; + $this->roleHierarchy = $roleHierarchy; + } + + public function supportsAttribute(string $attribute): bool + { + return false; + } + + public function supportsType(string $subjectType): bool + { + return true; + } + + public function vote(TokenInterface $token, mixed $subject, array $attributes): int + { + $result = VoterInterface::ACCESS_ABSTAIN; + $variables = null; + foreach ($attributes as $attribute) { + if (!$attribute instanceof Expression) { + continue; + } + + $variables ??= $this->getVariables($token, $subject); + + $result = VoterInterface::ACCESS_DENIED; + if ($this->expressionLanguage->evaluate($attribute, $variables)) { + return VoterInterface::ACCESS_GRANTED; + } + } + + return $result; + } + + private function getVariables(TokenInterface $token, mixed $subject): array + { + $roleNames = $token->getRoleNames(); + + if (null !== $this->roleHierarchy) { + $roleNames = $this->roleHierarchy->getReachableRoleNames($roleNames); + } + + $variables = [ + 'token' => $token, + 'user' => $token->getUser(), + 'object' => $subject, + 'subject' => $subject, + 'role_names' => $roleNames, + 'trust_resolver' => $this->trustResolver, + 'auth_checker' => $this->authChecker, + ]; + + // this is mainly to propose a better experience when the expression is used + // in an access control rule, as the developer does not know that it's going + // to be handled by this voter + if ($subject instanceof Request) { + $variables['request'] = $subject; + } + + return $variables; + } +} diff --git a/lib/symfony/security-core/Authorization/Voter/RoleHierarchyVoter.php b/lib/symfony/security-core/Authorization/Voter/RoleHierarchyVoter.php new file mode 100644 index 0000000000..c8db1485e0 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/RoleHierarchyVoter.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\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; + +/** + * RoleHierarchyVoter uses a RoleHierarchy to determine the roles granted to + * the user before voting. + * + * @author Fabien Potencier + */ +class RoleHierarchyVoter extends RoleVoter +{ + private RoleHierarchyInterface $roleHierarchy; + + public function __construct(RoleHierarchyInterface $roleHierarchy, string $prefix = 'ROLE_') + { + $this->roleHierarchy = $roleHierarchy; + + parent::__construct($prefix); + } + + /** + * @return array + */ + protected function extractRoles(TokenInterface $token) + { + return $this->roleHierarchy->getReachableRoleNames($token->getRoleNames()); + } +} diff --git a/lib/symfony/security-core/Authorization/Voter/RoleVoter.php b/lib/symfony/security-core/Authorization/Voter/RoleVoter.php new file mode 100644 index 0000000000..70dddcfff9 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/RoleVoter.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * RoleVoter votes if any attribute starts with a given prefix. + * + * @author Fabien Potencier + */ +class RoleVoter implements CacheableVoterInterface +{ + private string $prefix; + + public function __construct(string $prefix = 'ROLE_') + { + $this->prefix = $prefix; + } + + public function vote(TokenInterface $token, mixed $subject, array $attributes): int + { + $result = VoterInterface::ACCESS_ABSTAIN; + $roles = $this->extractRoles($token); + + foreach ($attributes as $attribute) { + if (!\is_string($attribute) || !str_starts_with($attribute, $this->prefix)) { + continue; + } + + $result = VoterInterface::ACCESS_DENIED; + foreach ($roles as $role) { + if ($attribute === $role) { + return VoterInterface::ACCESS_GRANTED; + } + } + } + + return $result; + } + + public function supportsAttribute(string $attribute): bool + { + return str_starts_with($attribute, $this->prefix); + } + + public function supportsType(string $subjectType): bool + { + return true; + } + + /** + * @return array + */ + protected function extractRoles(TokenInterface $token) + { + return $token->getRoleNames(); + } +} diff --git a/lib/symfony/security-core/Authorization/Voter/TraceableVoter.php b/lib/symfony/security-core/Authorization/Voter/TraceableVoter.php new file mode 100644 index 0000000000..412bb9760b --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/TraceableVoter.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Event\VoteEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * Decorates voter classes to send result events. + * + * @author Laurent VOULLEMIER + * + * @internal + */ +class TraceableVoter implements CacheableVoterInterface +{ + private VoterInterface $voter; + private EventDispatcherInterface $eventDispatcher; + + public function __construct(VoterInterface $voter, EventDispatcherInterface $eventDispatcher) + { + $this->voter = $voter; + $this->eventDispatcher = $eventDispatcher; + } + + public function vote(TokenInterface $token, mixed $subject, array $attributes): int + { + $result = $this->voter->vote($token, $subject, $attributes); + + $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $result), 'debug.security.authorization.vote'); + + return $result; + } + + public function getDecoratedVoter(): VoterInterface + { + return $this->voter; + } + + public function supportsAttribute(string $attribute): bool + { + return !$this->voter instanceof CacheableVoterInterface || $this->voter->supportsAttribute($attribute); + } + + public function supportsType(string $subjectType): bool + { + return !$this->voter instanceof CacheableVoterInterface || $this->voter->supportsType($subjectType); + } +} diff --git a/lib/symfony/security-core/Authorization/Voter/Voter.php b/lib/symfony/security-core/Authorization/Voter/Voter.php new file mode 100644 index 0000000000..1f76a42eaf --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/Voter.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Voter is an abstract default implementation of a voter. + * + * @author Roman Marintšenko + * @author Grégoire Pineau + * + * @template TAttribute of string + * @template TSubject of mixed + */ +abstract class Voter implements VoterInterface, CacheableVoterInterface +{ + public function vote(TokenInterface $token, mixed $subject, array $attributes): int + { + // abstain vote by default in case none of the attributes are supported + $vote = self::ACCESS_ABSTAIN; + + foreach ($attributes as $attribute) { + try { + if (!$this->supports($attribute, $subject)) { + continue; + } + } catch (\TypeError $e) { + if (str_contains($e->getMessage(), 'supports(): Argument #1')) { + continue; + } + + throw $e; + } + + // as soon as at least one attribute is supported, default is to deny access + $vote = self::ACCESS_DENIED; + + if ($this->voteOnAttribute($attribute, $subject, $token)) { + // grant access as soon as at least one attribute returns a positive response + return self::ACCESS_GRANTED; + } + } + + return $vote; + } + + /** + * Return false if your voter doesn't support the given attribute. Symfony will cache + * that decision and won't call your voter again for that attribute. + */ + public function supportsAttribute(string $attribute): bool + { + return true; + } + + /** + * Return false if your voter doesn't support the given subject type. Symfony will cache + * that decision and won't call your voter again for that subject type. + * + * @param string $subjectType The type of the subject inferred by `get_class()` or `get_debug_type()` + */ + public function supportsType(string $subjectType): bool + { + return true; + } + + /** + * Determines if the attribute and subject are supported by this voter. + * + * @param mixed $subject The subject to secure, e.g. an object the user wants to access or any other PHP type + * + * @psalm-assert-if-true TSubject $subject + * @psalm-assert-if-true TAttribute $attribute + */ + abstract protected function supports(string $attribute, mixed $subject): bool; + + /** + * Perform a single access check operation on a given attribute, subject and token. + * It is safe to assume that $attribute and $subject already passed the "supports()" method check. + * + * @param TAttribute $attribute + * @param TSubject $subject + */ + abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool; +} diff --git a/lib/symfony/security-core/Authorization/Voter/VoterInterface.php b/lib/symfony/security-core/Authorization/Voter/VoterInterface.php new file mode 100644 index 0000000000..8eea57e769 --- /dev/null +++ b/lib/symfony/security-core/Authorization/Voter/VoterInterface.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\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * VoterInterface is the interface implemented by all voters. + * + * @author Fabien Potencier + */ +interface VoterInterface +{ + public const ACCESS_GRANTED = 1; + public const ACCESS_ABSTAIN = 0; + public const ACCESS_DENIED = -1; + + /** + * Returns the vote for the given parameters. + * + * This method must return one of the following constants: + * ACCESS_GRANTED, ACCESS_DENIED, or ACCESS_ABSTAIN. + * + * @param mixed $subject The subject to secure + * @param array $attributes An array of attributes associated with the method being invoked + * + * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED + * + * @psalm-return self::ACCESS_* must be transformed into @return on Symfony 7 + */ + public function vote(TokenInterface $token, mixed $subject, array $attributes); +} diff --git a/lib/symfony/security-core/CHANGELOG.md b/lib/symfony/security-core/CHANGELOG.md new file mode 100644 index 0000000000..5c56c2f79c --- /dev/null +++ b/lib/symfony/security-core/CHANGELOG.md @@ -0,0 +1,65 @@ +CHANGELOG +========= + +6.4 +--- + + * Make `PersistentToken` immutable + * Deprecate accepting only `DateTime` for `TokenProviderInterface::updateToken()`, use `DateTimeInterface` instead + +6.3 +--- + + * Add `AttributesBasedUserProviderInterface` to allow `$attributes` optional argument on `loadUserByIdentifier` + * Add `OidcUser` with OIDC support for `OidcUserInfoTokenHandler` + +6.2 +--- + + * Deprecate the `Security` class, use `Symfony\Bundle\SecurityBundle\Security` instead + * Change the signature of `TokenStorageInterface::setToken()` to `setToken(?TokenInterface $token)` + * Deprecate calling `TokenStorage::setToken()` without arguments + * Add a `ChainUserChecker` to allow calling multiple user checkers for a firewall + +6.0 +--- + + * `TokenInterface` does not extend `Serializable` anymore + * Remove all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead + * Remove methods `getPassword()` and `getSalt()` from `UserInterface`, use `PasswordAuthenticatedUserInterface` + or `LegacyPasswordAuthenticatedUserInterface` instead +* `AccessDecisionManager` requires the strategy to be passed as in instance of `AccessDecisionStrategyInterface` + +5.4.21 +------ + + * [BC BREAK] `AccessDecisionStrategyTestCase::provideStrategyTests()` is now static + +5.4 +--- + + * Add a `CacheableVoterInterface` for voters that vote only on identified attributes and subjects + * Deprecate `AuthenticationEvents::AUTHENTICATION_FAILURE`, use the `LoginFailureEvent` instead + * Deprecate `AnonymousToken`, as the related authenticator was deprecated in 5.3 + * Deprecate `Token::getCredentials()`, tokens should no longer contain credentials (as they represent authenticated sessions) + * Deprecate returning `string|\Stringable` from `Token::getUser()` (it must return a `UserInterface`) + * Deprecate `AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY` and `AuthenticatedVoter::IS_ANONYMOUS`, + use `AuthenticatedVoter::IS_AUTHENTICATED_FULLY` or `AuthenticatedVoter::IS_AUTHENTICATED` instead. + * Deprecate `AuthenticationTrustResolverInterface::isAnonymous()` and the `is_anonymous()` expression + function as anonymous no longer exists in version 6, use the `isFullFledged()` or the new + `isAuthenticated()` instead if you want to check if the request is (fully) authenticated. + * Deprecate the `$authenticationManager` argument of the `AuthorizationChecker` constructor + * Deprecate setting the `$alwaysAuthenticate` argument to `true` and not setting the + `$exceptionOnNoToken` argument to `false` of `AuthorizationChecker` + * Deprecate methods `TokenInterface::isAuthenticated()` and `setAuthenticated`, + return null from "getUser()" instead when a token is not authenticated + * Add `AccessDecisionStrategyInterface` to allow custom access decision strategies + * Add access decision strategies `AffirmativeStrategy`, `ConsensusStrategy`, `PriorityStrategy`, `UnanimousStrategy` + * Deprecate passing the strategy as string to `AccessDecisionManager`, + pass an instance of `AccessDecisionStrategyInterface` instead + * Flag `AccessDecisionManager` as `@final` + +5.3 +--- + +The CHANGELOG for version 5.3 and earlier can be found at https://github.com/symfony/symfony/blob/5.3/src/Symfony/Component/Security/CHANGELOG.md diff --git a/lib/symfony/security-core/Event/AuthenticationEvent.php b/lib/symfony/security-core/Event/AuthenticationEvent.php new file mode 100644 index 0000000000..054dd95728 --- /dev/null +++ b/lib/symfony/security-core/Event/AuthenticationEvent.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Event; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * This is a general purpose authentication event. + * + * @author Johannes M. Schmitt + */ +class AuthenticationEvent extends Event +{ + private TokenInterface $authenticationToken; + + public function __construct(TokenInterface $token) + { + $this->authenticationToken = $token; + } + + /** + * @return TokenInterface + */ + public function getAuthenticationToken() + { + return $this->authenticationToken; + } +} diff --git a/lib/symfony/security-core/Event/AuthenticationSuccessEvent.php b/lib/symfony/security-core/Event/AuthenticationSuccessEvent.php new file mode 100644 index 0000000000..50034d7108 --- /dev/null +++ b/lib/symfony/security-core/Event/AuthenticationSuccessEvent.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\Security\Core\Event; + +final class AuthenticationSuccessEvent extends AuthenticationEvent +{ +} diff --git a/lib/symfony/security-core/Event/VoteEvent.php b/lib/symfony/security-core/Event/VoteEvent.php new file mode 100644 index 0000000000..1b1d6a336d --- /dev/null +++ b/lib/symfony/security-core/Event/VoteEvent.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\Security\Core\Event; + +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * This event is dispatched on voter vote. + * + * @author Laurent VOULLEMIER + * + * @internal + */ +final class VoteEvent extends Event +{ + private VoterInterface $voter; + private mixed $subject; + private array $attributes; + private int $vote; + + public function __construct(VoterInterface $voter, mixed $subject, array $attributes, int $vote) + { + $this->voter = $voter; + $this->subject = $subject; + $this->attributes = $attributes; + $this->vote = $vote; + } + + public function getVoter(): VoterInterface + { + return $this->voter; + } + + public function getSubject(): mixed + { + return $this->subject; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getVote(): int + { + return $this->vote; + } +} diff --git a/lib/symfony/security-core/Exception/AccessDeniedException.php b/lib/symfony/security-core/Exception/AccessDeniedException.php new file mode 100644 index 0000000000..894013eb4b --- /dev/null +++ b/lib/symfony/security-core/Exception/AccessDeniedException.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\Security\Core\Exception; + +use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; + +/** + * AccessDeniedException is thrown when the account has not the required role. + * + * @author Fabien Potencier + */ +#[WithHttpStatus(403)] +class AccessDeniedException extends RuntimeException +{ + private array $attributes = []; + private mixed $subject = null; + + public function __construct(string $message = 'Access Denied.', ?\Throwable $previous = null, int $code = 403) + { + parent::__construct($message, $code, $previous); + } + + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @return void + */ + public function setAttributes(array|string $attributes) + { + $this->attributes = (array) $attributes; + } + + public function getSubject(): mixed + { + return $this->subject; + } + + /** + * @return void + */ + public function setSubject(mixed $subject) + { + $this->subject = $subject; + } +} diff --git a/lib/symfony/security-core/Exception/AccountExpiredException.php b/lib/symfony/security-core/Exception/AccountExpiredException.php new file mode 100644 index 0000000000..91ea122e76 --- /dev/null +++ b/lib/symfony/security-core/Exception/AccountExpiredException.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\Security\Core\Exception; + +/** + * AccountExpiredException is thrown when the user account has expired. + * + * @author Fabien Potencier + * @author Alexander + */ +class AccountExpiredException extends AccountStatusException +{ + public function getMessageKey(): string + { + return 'Account has expired.'; + } +} diff --git a/lib/symfony/security-core/Exception/AccountStatusException.php b/lib/symfony/security-core/Exception/AccountStatusException.php new file mode 100644 index 0000000000..5b064929ed --- /dev/null +++ b/lib/symfony/security-core/Exception/AccountStatusException.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\Security\Core\Exception; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * AccountStatusException is the base class for authentication exceptions + * caused by the user account status. + * + * @author Fabien Potencier + * @author Alexander + */ +abstract class AccountStatusException extends AuthenticationException +{ + private ?UserInterface $user = null; + + /** + * Get the user. + */ + public function getUser(): ?UserInterface + { + return $this->user; + } + + /** + * @return void + */ + public function setUser(UserInterface $user) + { + $this->user = $user; + } + + public function __serialize(): array + { + return [$this->user, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->user, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Exception/AuthenticationCredentialsNotFoundException.php b/lib/symfony/security-core/Exception/AuthenticationCredentialsNotFoundException.php new file mode 100644 index 0000000000..fc28e4e50e --- /dev/null +++ b/lib/symfony/security-core/Exception/AuthenticationCredentialsNotFoundException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * AuthenticationCredentialsNotFoundException is thrown when an authentication is rejected + * because no Token is available. + * + * @author Fabien Potencier + * @author Alexander + */ +class AuthenticationCredentialsNotFoundException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'Authentication credentials could not be found.'; + } +} diff --git a/lib/symfony/security-core/Exception/AuthenticationException.php b/lib/symfony/security-core/Exception/AuthenticationException.php new file mode 100644 index 0000000000..a69155e19f --- /dev/null +++ b/lib/symfony/security-core/Exception/AuthenticationException.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * AuthenticationException is the base class for all authentication exceptions. + * + * @author Fabien Potencier + * @author Alexander + */ +#[WithHttpStatus(401)] +class AuthenticationException extends RuntimeException +{ + private ?TokenInterface $token = null; + + public function getToken(): ?TokenInterface + { + return $this->token; + } + + /** + * @return void + */ + public function setToken(TokenInterface $token) + { + $this->token = $token; + } + + /** + * Returns all the necessary state of the object for serialization purposes. + * + * There is no need to serialize any entry, they should be returned as-is. + * If you extend this method, keep in mind you MUST guarantee parent data is present in the state. + * Here is an example of how to extend this method: + * + * public function __serialize(): array + * { + * return [$this->childAttribute, parent::__serialize()]; + * } + * + * + * @see __unserialize() + */ + public function __serialize(): array + { + return [$this->token, $this->code, $this->message, $this->file, $this->line]; + } + + /** + * Restores the object state from an array given by __serialize(). + * + * There is no need to unserialize any entry in $data, they are already ready-to-use. + * If you extend this method, keep in mind you MUST pass the parent data to its respective class. + * Here is an example of how to extend this method: + * + * public function __unserialize(array $data): void + * { + * [$this->childAttribute, $parentData] = $data; + * parent::__unserialize($parentData); + * } + * + * + * @see __serialize() + */ + public function __unserialize(array $data): void + { + [$this->token, $this->code, $this->message, $this->file, $this->line] = $data; + } + + /** + * Message key to be used by the translation component. + * + * @return string + */ + public function getMessageKey() + { + return 'An authentication exception occurred.'; + } + + /** + * Message data to be used by the translation component. + */ + public function getMessageData(): array + { + return []; + } +} diff --git a/lib/symfony/security-core/Exception/AuthenticationExpiredException.php b/lib/symfony/security-core/Exception/AuthenticationExpiredException.php new file mode 100644 index 0000000000..1d04c5e86f --- /dev/null +++ b/lib/symfony/security-core/Exception/AuthenticationExpiredException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * AuthenticationExpiredException is thrown when an authentication token becomes un-authenticated between requests. + * + * In practice, this is due to the User changing between requests (e.g. password changes), + * causes the token to become un-authenticated. + * + * @author Ryan Weaver + */ +class AuthenticationExpiredException extends AccountStatusException +{ + public function getMessageKey(): string + { + return 'Authentication expired because your account information has changed.'; + } +} diff --git a/lib/symfony/security-core/Exception/AuthenticationServiceException.php b/lib/symfony/security-core/Exception/AuthenticationServiceException.php new file mode 100644 index 0000000000..fa5042e15f --- /dev/null +++ b/lib/symfony/security-core/Exception/AuthenticationServiceException.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\Security\Core\Exception; + +/** + * AuthenticationServiceException is thrown when an authentication request could not be processed due to a system problem. + * + * @author Fabien Potencier + * @author Alexander + */ +class AuthenticationServiceException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'Authentication request could not be processed due to a system problem.'; + } +} diff --git a/lib/symfony/security-core/Exception/BadCredentialsException.php b/lib/symfony/security-core/Exception/BadCredentialsException.php new file mode 100644 index 0000000000..6aeed7b00e --- /dev/null +++ b/lib/symfony/security-core/Exception/BadCredentialsException.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\Security\Core\Exception; + +/** + * BadCredentialsException is thrown when the user credentials are invalid. + * + * @author Fabien Potencier + * @author Alexander + */ +class BadCredentialsException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'Invalid credentials.'; + } +} diff --git a/lib/symfony/security-core/Exception/CookieTheftException.php b/lib/symfony/security-core/Exception/CookieTheftException.php new file mode 100644 index 0000000000..a32f30d545 --- /dev/null +++ b/lib/symfony/security-core/Exception/CookieTheftException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * This exception is thrown when the RememberMeServices implementation + * detects that a presented cookie has already been used by someone else. + * + * @author Johannes M. Schmitt + * @author Alexander + */ +class CookieTheftException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'Cookie has already been used by someone else.'; + } +} diff --git a/lib/symfony/security-core/Exception/CredentialsExpiredException.php b/lib/symfony/security-core/Exception/CredentialsExpiredException.php new file mode 100644 index 0000000000..5018377292 --- /dev/null +++ b/lib/symfony/security-core/Exception/CredentialsExpiredException.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\Security\Core\Exception; + +/** + * CredentialsExpiredException is thrown when the user account credentials have expired. + * + * @author Fabien Potencier + * @author Alexander + */ +class CredentialsExpiredException extends AccountStatusException +{ + public function getMessageKey(): string + { + return 'Credentials have expired.'; + } +} diff --git a/lib/symfony/security-core/Exception/CustomUserMessageAccountStatusException.php b/lib/symfony/security-core/Exception/CustomUserMessageAccountStatusException.php new file mode 100644 index 0000000000..d1afd6383a --- /dev/null +++ b/lib/symfony/security-core/Exception/CustomUserMessageAccountStatusException.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * An authentication exception caused by the user account status + * where you can control the message shown to the user. + * + * Be sure that the message passed to this exception is something that + * can be shown safely to your user. In other words, avoid catching + * other exceptions and passing their message directly to this class. + * + * @author Vincent Langlet + */ +class CustomUserMessageAccountStatusException extends AccountStatusException +{ + private string $messageKey; + private array $messageData = []; + + public function __construct(string $message = '', array $messageData = [], int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->setSafeMessage($message, $messageData); + } + + /** + * Sets a message that will be shown to the user. + * + * @param string $messageKey The message or message key + * @param array $messageData Data to be passed into the translator + * + * @return void + */ + public function setSafeMessage(string $messageKey, array $messageData = []) + { + $this->messageKey = $messageKey; + $this->messageData = $messageData; + } + + public function getMessageKey(): string + { + return $this->messageKey; + } + + public function getMessageData(): array + { + return $this->messageData; + } + + public function __serialize(): array + { + return [parent::__serialize(), $this->messageKey, $this->messageData]; + } + + public function __unserialize(array $data): void + { + [$parentData, $this->messageKey, $this->messageData] = $data; + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Exception/CustomUserMessageAuthenticationException.php b/lib/symfony/security-core/Exception/CustomUserMessageAuthenticationException.php new file mode 100644 index 0000000000..efcaa4976a --- /dev/null +++ b/lib/symfony/security-core/Exception/CustomUserMessageAuthenticationException.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * An authentication exception where you can control the message shown to the user. + * + * Be sure that the message passed to this exception is something that + * can be shown safely to your user. In other words, avoid catching + * other exceptions and passing their message directly to this class. + * + * @author Ryan Weaver + */ +class CustomUserMessageAuthenticationException extends AuthenticationException +{ + private string $messageKey; + private array $messageData = []; + + public function __construct(string $message = '', array $messageData = [], int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->setSafeMessage($message, $messageData); + } + + /** + * Set a message that will be shown to the user. + * + * @param string $messageKey The message or message key + * @param array $messageData Data to be passed into the translator + * + * @return void + */ + public function setSafeMessage(string $messageKey, array $messageData = []) + { + $this->messageKey = $messageKey; + $this->messageData = $messageData; + } + + public function getMessageKey(): string + { + return $this->messageKey; + } + + public function getMessageData(): array + { + return $this->messageData; + } + + public function __serialize(): array + { + return [parent::__serialize(), $this->messageKey, $this->messageData]; + } + + public function __unserialize(array $data): void + { + [$parentData, $this->messageKey, $this->messageData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Exception/DisabledException.php b/lib/symfony/security-core/Exception/DisabledException.php new file mode 100644 index 0000000000..b82067cc42 --- /dev/null +++ b/lib/symfony/security-core/Exception/DisabledException.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\Security\Core\Exception; + +/** + * DisabledException is thrown when the user account is disabled. + * + * @author Fabien Potencier + * @author Alexander + */ +class DisabledException extends AccountStatusException +{ + public function getMessageKey(): string + { + return 'Account is disabled.'; + } +} diff --git a/lib/symfony/security-core/Exception/ExceptionInterface.php b/lib/symfony/security-core/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..7bc2b9132c --- /dev/null +++ b/lib/symfony/security-core/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * Base ExceptionInterface for the Security component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/lib/symfony/security-core/Exception/InsufficientAuthenticationException.php b/lib/symfony/security-core/Exception/InsufficientAuthenticationException.php new file mode 100644 index 0000000000..0221dfd22a --- /dev/null +++ b/lib/symfony/security-core/Exception/InsufficientAuthenticationException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * InsufficientAuthenticationException is thrown if the user credentials are not sufficiently trusted. + * + * This is the case when a user is anonymous and the resource to be displayed has an access role. + * + * @author Fabien Potencier + * @author Alexander + */ +class InsufficientAuthenticationException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'Not privileged to request the resource.'; + } +} diff --git a/lib/symfony/security-core/Exception/InvalidArgumentException.php b/lib/symfony/security-core/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..6f85e9500f --- /dev/null +++ b/lib/symfony/security-core/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * Base InvalidArgumentException for the Security component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/lib/symfony/security-core/Exception/InvalidCsrfTokenException.php b/lib/symfony/security-core/Exception/InvalidCsrfTokenException.php new file mode 100644 index 0000000000..2041cf6b8a --- /dev/null +++ b/lib/symfony/security-core/Exception/InvalidCsrfTokenException.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\Security\Core\Exception; + +/** + * This exception is thrown when the csrf token is invalid. + * + * @author Johannes M. Schmitt + * @author Alexander + */ +class InvalidCsrfTokenException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'Invalid CSRF token.'; + } +} diff --git a/lib/symfony/security-core/Exception/LazyResponseException.php b/lib/symfony/security-core/Exception/LazyResponseException.php new file mode 100644 index 0000000000..e26a3347c6 --- /dev/null +++ b/lib/symfony/security-core/Exception/LazyResponseException.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\Security\Core\Exception; + +use Symfony\Component\HttpFoundation\Response; + +/** + * A signaling exception that wraps a lazily computed response. + * + * @author Nicolas Grekas + */ +class LazyResponseException extends \Exception implements ExceptionInterface +{ + private Response $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse(): Response + { + return $this->response; + } +} diff --git a/lib/symfony/security-core/Exception/LockedException.php b/lib/symfony/security-core/Exception/LockedException.php new file mode 100644 index 0000000000..fb81cb0545 --- /dev/null +++ b/lib/symfony/security-core/Exception/LockedException.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\Security\Core\Exception; + +/** + * LockedException is thrown if the user account is locked. + * + * @author Fabien Potencier + * @author Alexander + */ +class LockedException extends AccountStatusException +{ + public function getMessageKey(): string + { + return 'Account is locked.'; + } +} diff --git a/lib/symfony/security-core/Exception/LogicException.php b/lib/symfony/security-core/Exception/LogicException.php new file mode 100644 index 0000000000..b9c63e941f --- /dev/null +++ b/lib/symfony/security-core/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * Base LogicException for the Security component. + * + * @author Iltar van der Berg + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/lib/symfony/security-core/Exception/LogoutException.php b/lib/symfony/security-core/Exception/LogoutException.php new file mode 100644 index 0000000000..20efdd267d --- /dev/null +++ b/lib/symfony/security-core/Exception/LogoutException.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\Security\Core\Exception; + +/** + * LogoutException is thrown when the account cannot be logged out. + * + * @author Jeremy Mikola + */ +class LogoutException extends RuntimeException +{ + public function __construct(string $message = 'Logout Exception', ?\Throwable $previous = null) + { + parent::__construct($message, 403, $previous); + } +} diff --git a/lib/symfony/security-core/Exception/ProviderNotFoundException.php b/lib/symfony/security-core/Exception/ProviderNotFoundException.php new file mode 100644 index 0000000000..e4daf4ef34 --- /dev/null +++ b/lib/symfony/security-core/Exception/ProviderNotFoundException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * ProviderNotFoundException is thrown when no AuthenticationProviderInterface instance + * supports an authentication Token. + * + * @author Fabien Potencier + * @author Alexander + */ +class ProviderNotFoundException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'No authentication provider found to support the authentication token.'; + } +} diff --git a/lib/symfony/security-core/Exception/RuntimeException.php b/lib/symfony/security-core/Exception/RuntimeException.php new file mode 100644 index 0000000000..95edec8ee9 --- /dev/null +++ b/lib/symfony/security-core/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * Base RuntimeException for the Security component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/lib/symfony/security-core/Exception/SessionUnavailableException.php b/lib/symfony/security-core/Exception/SessionUnavailableException.php new file mode 100644 index 0000000000..eec069c5d5 --- /dev/null +++ b/lib/symfony/security-core/Exception/SessionUnavailableException.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\Security\Core\Exception; + +/** + * This exception is thrown when no session is available. + * + * Possible reasons for this are: + * + * a) The session timed out because the user waited too long. + * b) The user has disabled cookies, and a new session is started on each + * request. + * + * @author Johannes M. Schmitt + * @author Alexander + */ +class SessionUnavailableException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'No session available, it either timed out or cookies are not enabled.'; + } +} diff --git a/lib/symfony/security-core/Exception/TokenNotFoundException.php b/lib/symfony/security-core/Exception/TokenNotFoundException.php new file mode 100644 index 0000000000..a18f0d080a --- /dev/null +++ b/lib/symfony/security-core/Exception/TokenNotFoundException.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\Security\Core\Exception; + +/** + * TokenNotFoundException is thrown if a Token cannot be found. + * + * @author Johannes M. Schmitt + * @author Alexander + */ +class TokenNotFoundException extends AuthenticationException +{ + public function getMessageKey(): string + { + return 'No token could be found.'; + } +} diff --git a/lib/symfony/security-core/Exception/TooManyLoginAttemptsAuthenticationException.php b/lib/symfony/security-core/Exception/TooManyLoginAttemptsAuthenticationException.php new file mode 100644 index 0000000000..da1a1a7a68 --- /dev/null +++ b/lib/symfony/security-core/Exception/TooManyLoginAttemptsAuthenticationException.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * This exception is thrown if there where too many failed login attempts in + * this session. + * + * @author Wouter de Jong + */ +class TooManyLoginAttemptsAuthenticationException extends AuthenticationException +{ + private ?int $threshold; + + public function __construct(?int $threshold = null) + { + $this->threshold = $threshold; + } + + public function getMessageData(): array + { + return [ + '%minutes%' => $this->threshold, + '%count%' => (int) $this->threshold, + ]; + } + + public function getMessageKey(): string + { + return 'Too many failed login attempts, please try again '.($this->threshold ? 'in %minutes% minute'.($this->threshold > 1 ? 's' : '').'.' : 'later.'); + } + + public function __serialize(): array + { + return [$this->threshold, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->threshold, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/Exception/UnsupportedUserException.php b/lib/symfony/security-core/Exception/UnsupportedUserException.php new file mode 100644 index 0000000000..6529fa9f0b --- /dev/null +++ b/lib/symfony/security-core/Exception/UnsupportedUserException.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\Security\Core\Exception; + +/** + * This exception is thrown when an account is reloaded from a provider which + * doesn't support the passed implementation of UserInterface. + * + * @author Johannes M. Schmitt + */ +class UnsupportedUserException extends AuthenticationServiceException +{ +} diff --git a/lib/symfony/security-core/Exception/UserNotFoundException.php b/lib/symfony/security-core/Exception/UserNotFoundException.php new file mode 100644 index 0000000000..6cd9b71275 --- /dev/null +++ b/lib/symfony/security-core/Exception/UserNotFoundException.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * UserNotFoundException is thrown if a User cannot be found for the given identifier. + * + * @author Fabien Potencier + * @author Alexander + */ +class UserNotFoundException extends AuthenticationException +{ + private ?string $identifier = null; + + public function getMessageKey(): string + { + return 'Username could not be found.'; + } + + /** + * Get the user identifier (e.g. username or email address). + */ + public function getUserIdentifier(): ?string + { + return $this->identifier; + } + + /** + * Set the user identifier (e.g. username or email address). + */ + public function setUserIdentifier(string $identifier): void + { + $this->identifier = $identifier; + } + + public function getMessageData(): array + { + return ['{{ username }}' => $this->identifier, '{{ user_identifier }}' => $this->identifier]; + } + + public function __serialize(): array + { + return [$this->identifier, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->identifier, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/security-core/LICENSE b/lib/symfony/security-core/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/security-core/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/security-core/README.md b/lib/symfony/security-core/README.md new file mode 100644 index 0000000000..48ffb0e526 --- /dev/null +++ b/lib/symfony/security-core/README.md @@ -0,0 +1,63 @@ +Security Component - Core +========================= + +Security provides an infrastructure for sophisticated authorization systems, +which makes it possible to easily separate the actual authorization logic from +so called user providers that hold the users credentials. + +Getting Started +--------------- + +``` +$ composer require symfony/security-core +``` + +```php +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Role\RoleHierarchy; + +$accessDecisionManager = new AccessDecisionManager([ + new AuthenticatedVoter(new AuthenticationTrustResolver()), + new RoleVoter(), + new RoleHierarchyVoter(new RoleHierarchy([ + 'ROLE_ADMIN' => ['ROLE_USER'], + ])) +]); + +$user = new \App\Entity\User(...); +$token = new UsernamePasswordToken($user, 'main', $user->getRoles()); + +if (!$accessDecisionManager->decide($token, ['ROLE_ADMIN'])) { + throw new AccessDeniedException(); +} +``` + +Sponsor +------- + +The Security component for Symfony 6.4 is [backed][1] by [SymfonyCasts][2]. + +Learn Symfony faster by watching real projects being built and actively coding +along with them. SymfonyCasts bridges that learning gap, bringing you video +tutorials and coding challenges. Code on! + +Help Symfony by [sponsoring][3] its development! + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/security.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) + +[1]: https://symfony.com/backers +[2]: https://symfonycasts.com +[3]: https://symfony.com/sponsor diff --git a/lib/symfony/security-core/Resources/translations/security.af.xlf b/lib/symfony/security-core/Resources/translations/security.af.xlf new file mode 100644 index 0000000000..7bcb92066c --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.af.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + 'n Verifikasie probleem het voorgekom. + + + Authentication credentials could not be found. + Verifikasiebewyse kon nie gevind word nie. + + + Authentication request could not be processed due to a system problem. + Verifikasieversoek kon weens 'n stelselprobleem nie verwerk word nie. + + + Invalid credentials. + Ongedige verifikasiebewyse. + + + Cookie has already been used by someone else. + Die koekie is alreeds deur iemand anders gebruik. + + + Not privileged to request the resource. + Nie bevoorreg om die hulpbron aan te vra nie. + + + Invalid CSRF token. + Ongeldige CSRF-teken. + + + No authentication provider found to support the authentication token. + Geen verifikasieverskaffer is gevind wat die verifikasietoken kan ondersteun nie. + + + No session available, it either timed out or cookies are not enabled. + Geen sessie is beskikbaar, die het verval of koekies is nie geaktiveer nie. + + + No token could be found. + Geen teken kon gevind word nie. + + + Username could not be found. + Gebruikersnaam kon nie gevind word nie. + + + Account has expired. + Rekening het verval. + + + Credentials have expired. + Verifikasiebewyse het verval. + + + Account is disabled. + Rekening is deaktiveer. + + + Account is locked. + Rekening is gesluit. + + + Too many failed login attempts, please try again later. + Te veel mislukte aanmeldpogings, probeer asseblief later weer. + + + Invalid or expired login link. + Ongeldige of vervalde aanmeldskakel. + + + Too many failed login attempts, please try again in %minutes% minute. + Te veel mislukte aanmeldpogings, probeer asseblief weer oor %minutes% minuut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Te veel mislukte aanmeldpogings, probeer asseblief weer oor %minutes% minute. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.ar.xlf b/lib/symfony/security-core/Resources/translations/security.ar.xlf new file mode 100644 index 0000000000..f75eb12c00 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.ar.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + حدث خطأ اثناء الدخول. + + + Authentication credentials could not be found. + لم استطع العثور على معلومات الدخول. + + + Authentication request could not be processed due to a system problem. + لم يكتمل طلب الدخول نتيجه عطل فى النظام. + + + Invalid credentials. + معلومات الدخول خاطئة. + + + Cookie has already been used by someone else. + ملفات تعريف الارتباط(cookies) تم استخدامها من قبل شخص اخر. + + + Not privileged to request the resource. + ليست لديك الصلاحيات الكافية لهذا الطلب. + + + Invalid CSRF token. + رمز الموقع غير صحيح. + + + No authentication provider found to support the authentication token. + لا يوجد معرف للدخول يدعم الرمز المستخدم للدخول. + + + No session available, it either timed out or cookies are not enabled. + لا يوجد صلة بينك و بين الموقع اما انها انتهت او ان متصفحك لا يدعم خاصية ملفات تعريف الارتباط (cookies). + + + No token could be found. + لم استطع العثور على الرمز. + + + Username could not be found. + لم استطع العثور على اسم الدخول. + + + Account has expired. + انتهت صلاحية الحساب. + + + Credentials have expired. + انتهت صلاحية معلومات الدخول. + + + Account is disabled. + الحساب موقوف. + + + Account is locked. + الحساب مغلق. + + + Too many failed login attempts, please try again later. + العديد من محاولات الدخول الفاشلة، يرجى المحاولة مرة أخرى في وقت لاحق. + + + Invalid or expired login link. + رابط تسجيل الدخول غير صالح أو منتهي الصلاحية. + + + Too many failed login attempts, please try again in %minutes% minute. + العديد من محاولات الدخول الفاشلة، يرجى اعادة المحاولة بعد %minutes% دقيقة. + + + Too many failed login attempts, please try again in %minutes% minutes. + العديد من محاولات الدخول الفاشلة ، يرجى اعادة المحاولة بعد %minutes% دقائق. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.az.xlf b/lib/symfony/security-core/Resources/translations/security.az.xlf new file mode 100644 index 0000000000..25cb8605d2 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.az.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Doğrulama istisnası baş verdi. + + + Authentication credentials could not be found. + Doğrulama məlumatları tapılmadı. + + + Authentication request could not be processed due to a system problem. + Sistem xətası səbəbilə doğrulama istəyi emal edilə bilmədi. + + + Invalid credentials. + Yanlış məlumat. + + + Cookie has already been used by someone else. + Kuki başqası tərəfindən istifadə edilib. + + + Not privileged to request the resource. + Resurs istəyi üçün imtiyaz yoxdur. + + + Invalid CSRF token. + Yanlış CSRF nişanı. + + + No authentication provider found to support the authentication token. + Doğrulama nişanını dəstəkləyəcək provayder tapılmadı. + + + No session available, it either timed out or cookies are not enabled. + Uyğun seans yoxdur, vaxtı keçib və ya kuki aktiv deyil. + + + No token could be found. + Nişan tapılmadı. + + + Username could not be found. + İstifadəçi adı tapılmadı. + + + Account has expired. + Hesabın istifadə müddəti bitib. + + + Credentials have expired. + Məlumatların istifadə müddəti bitib. + + + Account is disabled. + Hesab qeyri-aktiv edilib. + + + Account is locked. + Hesab kilitlənib. + + + Too many failed login attempts, please try again later. + Çoxlu uğursuz giriş təşəbbüsü, zəhmət olmasa daha sonra yeniden yoxlayın. + + + Invalid or expired login link. + Yanlış və ya müddəti keçmiş giriş keçidi. + + + Too many failed login attempts, please try again in %minutes% minute. + Həddindən artıq uğursuz giriş cəhdi, lütfən %minutes% dəqiqə ərzində yenidən yoxlayın. + + + Too many failed login attempts, please try again in %minutes% minutes. + Çox sayda uğursuz giriş cəhdi, zəhmət olmasa %minutes% dəqiqə sonra yenidən cəhd edin.|Çox sayda uğursuz giriş cəhdi, zəhmət olmasa %minutes% dəqiqə sonra yenidən cəhd edin. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.be.xlf b/lib/symfony/security-core/Resources/translations/security.be.xlf new file mode 100644 index 0000000000..6478e2a15c --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.be.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Памылка аўтэнтыфікацыі. + + + Authentication credentials could not be found. + Дадзеныя аўтэнтыфікацыі не знойдзены. + + + Authentication request could not be processed due to a system problem. + Запыт аўтэнтыфікацыі не можа быць апрацаваны ў сувязі з праблемай у сістэме. + + + Invalid credentials. + Несапраўдныя дадзеныя аўтэнтыфікацыі. + + + Cookie has already been used by someone else. + Нехта іншы ўжо выкарыстаў гэтыя кукі (cookie). + + + Not privileged to request the resource. + Адсутнічаюць правы на запыт гэтага рэсурсу. + + + Invalid CSRF token. + Несапраўдны CSRF-токен. + + + No authentication provider found to support the authentication token. + Не знойдзен правайдар аўтэнтыфікацыі, які можа падтрымліваць гэты токен аўтэнтыфікацыі. + + + No session available, it either timed out or cookies are not enabled. + Сесія не даступна, яе час скончыўся, або кукі (cookies) выключаны. + + + No token could be found. + Токен не знойдзен. + + + Username could not be found. + Імя карыстальніка не знойдзена. + + + Account has expired. + Скончыўся тэрмін дзеяння акаўнта. + + + Credentials have expired. + Скончыўся тэрмін дзеяння дадзеных аўтэнтыфікацыі. + + + Account is disabled. + Акаўнт адключан. + + + Account is locked. + Акаўнт заблакіраван. + + + Too many failed login attempts, please try again later. + Зашмат няўдалых спроб уваходу, калі ласка, паспрабуйце пазней. + + + Invalid or expired login link. + Спасылка для ўваходу несапраўдная або пратэрмінаваная. + + + Too many failed login attempts, please try again in %minutes% minute. + Занадта шмат няўдалых спроб уваходу ў сістэму, паспрабуйце спробу праз %minutes% хвіліну. + + + Too many failed login attempts, please try again in %minutes% minutes. + Занадта вялікая колькасць няўдалых спробаў уваходу. Калі ласка, паспрабуйце зноў праз %minutes% хвіліну.|Занадта вялікая колькасць няўдалых спробаў уваходу. Калі ласка, паспрабуйце зноў праз %minutes% хвіліны.|Занадта вялікая колькасць няўдалых спробаў уваходу. Калі ласка, паспрабуйце зноў праз %minutes% хвілін. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.bg.xlf b/lib/symfony/security-core/Resources/translations/security.bg.xlf new file mode 100644 index 0000000000..5c49168ceb --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.bg.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Грешка при автентикация. + + + Authentication credentials could not be found. + Удостоверението за автентикация не е открито. + + + Authentication request could not be processed due to a system problem. + Заявката за автентикация не може да бъде обработената поради системна грешка. + + + Invalid credentials. + Невалидно удостоверение за автентикация. + + + Cookie has already been used by someone else. + Тази бисквитка вече се ползва от някой друг. + + + Not privileged to request the resource. + Нямате права за достъп до този ресурс. + + + Invalid CSRF token. + Невалиден CSRF токен. + + + No authentication provider found to support the authentication token. + Не е открит провайдър, който да поддържа този токен за автентикация. + + + No session available, it either timed out or cookies are not enabled. + Сесията не е достъпна, или времето за достъп е изтекло, или бисквитките не са разрешени. + + + No token could be found. + Токенът не е открит. + + + Username could not be found. + Потребителското име не е открито. + + + Account has expired. + Акаунтът е изтекъл. + + + Credentials have expired. + Удостоверението за автентикация е изтекло. + + + Account is disabled. + Акаунтът е деактивиран. + + + Account is locked. + Акаунтът е заключен. + + + Too many failed login attempts, please try again later. + Твърде много неуспешни опити за вход, моля опитайте по-късно. + + + Invalid or expired login link. + Невалиден или изтекъл линк за вход. + + + Too many failed login attempts, please try again in %minutes% minute. + Твърде много неуспешни опити за вход, моля опитайте отново след %minutes% минута. + + + Too many failed login attempts, please try again in %minutes% minutes. + Твърде много неуспешни опити за вход, моля опитайте отново след %minutes% минути. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.bs.xlf b/lib/symfony/security-core/Resources/translations/security.bs.xlf new file mode 100644 index 0000000000..f58dce0ea8 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.bs.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Došlo je do autentifikacijskog izuzetka (exception). + + + Authentication credentials could not be found. + Autentifikacijski podaci nisu pronađeni. + + + Authentication request could not be processed due to a system problem. + Autentifikacijski zahtjev ne može biti obrađen zbog sistemskog problema. + + + Invalid credentials. + Autentifikacijski podaci su neispravni. + + + Cookie has already been used by someone else. + Neko drugi je već iskoristio ovaj kolačić (cookie). + + + Not privileged to request the resource. + Nemate privilegije potrebne za pristup ovom resursu. + + + Invalid CSRF token. + CSRF žeton (token) je neispravan. + + + No authentication provider found to support the authentication token. + Nije pronađen autentifikacijski provajder koji bi podržao dati autentifikacijski žeton (token). + + + No session available, it either timed out or cookies are not enabled. + Nema dostupnih sesija; ili je istekla ili su kolačići (cookies) iksljučeni. + + + No token could be found. + Nije pronađen nijedan žeton (token). + + + Username could not be found. + Korisničko ime nije pronađeno. + + + Account has expired. + Nalog je istekao. + + + Credentials have expired. + Autentifikacijski podaci su istekli. + + + Account is disabled. + Nalog je onemogućen. + + + Account is locked. + Nalog je zaključan. + + + Too many failed login attempts, please try again later. + Previše neuspješnih pokušaja prijavljivanja, molim pokušajte ponovo kasnije. + + + Invalid or expired login link. + Link za prijavljivanje je istekao ili je neispravan. + + + Too many failed login attempts, please try again in %minutes% minute. + Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minuta. + + + Too many failed login attempts, please try again in %minutes% minutes. + Previše neuspješnih pokušaja prijave, pokušajte ponovo za %minutes% minut.|Previše neuspješnih pokušaja prijave, pokušajte ponovo za %minutes% minuta. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.ca.xlf b/lib/symfony/security-core/Resources/translations/security.ca.xlf new file mode 100644 index 0000000000..93ff24f330 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.ca.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ha succeït un error d'autenticació. + + + Authentication credentials could not be found. + No s'han trobat les credencials d'autenticació. + + + Authentication request could not be processed due to a system problem. + La solicitud d'autenticació no s'ha pogut processar per un problema del sistema. + + + Invalid credentials. + Credencials no vàlides. + + + Cookie has already been used by someone else. + La cookie ja ha estat utilitzada per una altra persona. + + + Not privileged to request the resource. + No té privilegis per solicitar el recurs. + + + Invalid CSRF token. + Token CSRF no vàlid. + + + No authentication provider found to support the authentication token. + No s'ha trobat un proveïdor d'autenticació que suporti el token d'autenticació. + + + No session available, it either timed out or cookies are not enabled. + No hi ha sessió disponible, ha expirat o les cookies no estan habilitades. + + + No token could be found. + No s'ha trobat cap token. + + + Username could not be found. + No s'ha trobat el nom d'usuari. + + + Account has expired. + El compte ha expirat. + + + Credentials have expired. + Les credencials han expirat. + + + Account is disabled. + El compte està deshabilitat. + + + Account is locked. + El compte està bloquejat. + + + Too many failed login attempts, please try again later. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar més tard. + + + Invalid or expired login link. + Enllaç d'inici de sessió no vàlid o caducat. + + + Too many failed login attempts, please try again in %minutes% minute. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar en %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar en %minutes% minuts. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.cs.xlf b/lib/symfony/security-core/Resources/translations/security.cs.xlf new file mode 100644 index 0000000000..213d2975a7 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.cs.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Při ověřování došlo k chybě. + + + Authentication credentials could not be found. + Ověřovací údaje nebyly nalezeny. + + + Authentication request could not be processed due to a system problem. + Požadavek na ověření nemohl být zpracován kvůli systémové chybě. + + + Invalid credentials. + Neplatné přihlašovací údaje. + + + Cookie has already been used by someone else. + Cookie již bylo použité někým jiným. + + + Not privileged to request the resource. + Nemáte oprávnění přistupovat k prostředku. + + + Invalid CSRF token. + Neplatný CSRF token. + + + No authentication provider found to support the authentication token. + Poskytovatel pro ověřovací token nebyl nalezen. + + + No session available, it either timed out or cookies are not enabled. + Session není k dispozici, vypršela její platnost, nebo jsou zakázané cookies. + + + No token could be found. + Token nebyl nalezen. + + + Username could not be found. + Přihlašovací jméno nebylo nalezeno. + + + Account has expired. + Platnost účtu vypršela. + + + Credentials have expired. + Platnost přihlašovacích údajů vypršela. + + + Account is disabled. + Účet je zakázaný. + + + Account is locked. + Účet je zablokovaný. + + + Too many failed login attempts, please try again later. + Příliš mnoho nepovedených pokusů přihlášení. Zkuste to prosím později. + + + Invalid or expired login link. + Neplatný nebo expirovaný odkaz na přihlášení. + + + Too many failed login attempts, please try again in %minutes% minute. + Příliš mnoho neúspěšných pokusů o přihlášení, zkuste to prosím znovu za %minutes% minutu. + + + Too many failed login attempts, please try again in %minutes% minutes. + Příliš mnoho neúspěšných pokusů o přihlášení, zkuste to prosím znovu za %minutes% minutu.|Příliš mnoho neúspěšných pokusů o přihlášení, zkuste to prosím znovu za %minutes% minuty.|Příliš mnoho neúspěšných pokusů o přihlášení, zkuste to prosím znovu za %minutes% minut. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.cy.xlf b/lib/symfony/security-core/Resources/translations/security.cy.xlf new file mode 100644 index 0000000000..ddb47097a9 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.cy.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Digwyddodd eithriad dilysu. + + + Authentication credentials could not be found. + Ni ellid dod o hyd i ddogfennau dilysu. + + + Authentication request could not be processed due to a system problem. + Ni ellid prosesu cais dilysu oherwydd problem gyda'r system. + + + Invalid credentials. + Dogfennau annilys. + + + Cookie has already been used by someone else. + Mae rhywun arall eisoes wedi defnyddio'r cwcis. + + + Not privileged to request the resource. + Heb y fraint i ofyn am yr adnodd. + + + Invalid CSRF token. + Tocyn CSRF annilys. + + + No authentication provider found to support the authentication token. + Heb ddod o hyd i ddarparwr dilysu i gefnogi'r tocyn dilysu. + + + No session available, it either timed out or cookies are not enabled. + Dim sesiwn ar gael, naill ai mae wedi dod i ben neu nid yw cwcis wedi'u galluogi. + + + No token could be found. + Heb ddod o hyd i docyn. + + + Username could not be found. + Heb ddod o hyd i enw defnyddiwr. + + + Account has expired. + Mae'r cyfrif wedi dod i ben. + + + Credentials have expired. + Mae'r dogfennau wedi dod i ben. + + + Account is disabled. + Mae'r cyfrif wedi'i analluogi. + + + Account is locked. + Mae'r cyfrif wedi'i gloi. + + + Too many failed login attempts, please try again later. + Gormod o ymdrechion mewngofnodi wedi methu, ceisiwch eto'n hwyrach. + + + Invalid or expired login link. + Dolen mewngofnodi annilys neu wedi dod i ben. + + + Too many failed login attempts, please try again in %minutes% minute. + Gormod o ymdrechion mewngofnodi wedi methu, ceisiwch eto ymhen %minutes% munud. + + + Too many failed login attempts, please try again in %minutes% minutes. + Gormod o ymdrechion mewngofnodi wedi methu, rhowch gynnig arall arni mewn %minutes% munud.|Gormod o ymdrechion mewngofnodi wedi methu, rhowch gynnig arall arni mewn %minutes% munud. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.da.xlf b/lib/symfony/security-core/Resources/translations/security.da.xlf new file mode 100644 index 0000000000..564f0eee99 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.da.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + En fejl indtraf ved godkendelse. + + + Authentication credentials could not be found. + Loginoplysninger kunne ikke findes. + + + Authentication request could not be processed due to a system problem. + Godkendelsesanmodningen kunne ikke behandles på grund af en systemfejl. + + + Invalid credentials. + Ugyldige loginoplysninger. + + + Cookie has already been used by someone else. + Cookie er allerede blevet brugt af en anden. + + + Not privileged to request the resource. + Ingen adgang til at forespørge ressourcen. + + + Invalid CSRF token. + Ugyldig CSRF-token. + + + No authentication provider found to support the authentication token. + Ingen godkendelsesudbyder blev fundet til at understøtte godkendelsestoken. + + + No session available, it either timed out or cookies are not enabled. + Ingen session er tilgængelig. Den er enten udløbet eller cookies er ikke aktiveret. + + + No token could be found. + Ingen token kunne findes. + + + Username could not be found. + Brugernavn kunne ikke findes. + + + Account has expired. + Brugerkonto er udløbet. + + + Credentials have expired. + Loginoplysninger er udløbet. + + + Account is disabled. + Brugerkonto er deaktiveret. + + + Account is locked. + Brugerkonto er låst. + + + Too many failed login attempts, please try again later. + For mange mislykkede loginforsøg. Prøv venligst igen senere. + + + Invalid or expired login link. + Ugyldigt eller udløbet login-link. + + + Too many failed login attempts, please try again in %minutes% minute. + For mange mislykkede loginforsøg. Prøv venligst igen om %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + For mange mislykkede loginforsøg, prøv igen om %minutes% minutter. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.de.xlf b/lib/symfony/security-core/Resources/translations/security.de.xlf new file mode 100644 index 0000000000..c1c457abd9 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.de.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Es ist ein Fehler bei der Authentifikation aufgetreten. + + + Authentication credentials could not be found. + Es konnten keine Zugangsdaten gefunden werden. + + + Authentication request could not be processed due to a system problem. + Die Authentifikation konnte wegen eines Systemproblems nicht bearbeitet werden. + + + Invalid credentials. + Fehlerhafte Zugangsdaten. + + + Cookie has already been used by someone else. + Cookie wurde bereits von jemand anderem verwendet. + + + Not privileged to request the resource. + Keine Rechte, um die Ressource anzufragen. + + + Invalid CSRF token. + Ungültiges CSRF-Token. + + + No authentication provider found to support the authentication token. + Es wurde kein Authentifizierungs-Provider gefunden, der das Authentifizierungs-Token unterstützt. + + + No session available, it either timed out or cookies are not enabled. + Keine Session verfügbar, entweder ist diese abgelaufen oder Cookies sind nicht aktiviert. + + + No token could be found. + Es wurde kein Token gefunden. + + + Username could not be found. + Der Benutzername wurde nicht gefunden. + + + Account has expired. + Der Account ist abgelaufen. + + + Credentials have expired. + Die Zugangsdaten sind abgelaufen. + + + Account is disabled. + Der Account ist deaktiviert. + + + Account is locked. + Der Account ist gesperrt. + + + Too many failed login attempts, please try again later. + Zu viele fehlgeschlagene Anmeldeversuche, bitte versuchen Sie es später noch einmal. + + + Invalid or expired login link. + Ungültiger oder abgelaufener Anmelde-Link. + + + Too many failed login attempts, please try again in %minutes% minute. + Zu viele fehlgeschlagene Anmeldeversuche, bitte versuchen Sie es in einer Minute noch einmal. + + + Too many failed login attempts, please try again in %minutes% minutes. + Zu viele fehlgeschlagene Anmeldeversuche, bitte versuchen Sie es in %minutes% Minuten noch einmal. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.el.xlf b/lib/symfony/security-core/Resources/translations/security.el.xlf new file mode 100644 index 0000000000..25cfb43bdf --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.el.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Συνέβη ένα σφάλμα πιστοποίησης. + + + Authentication credentials could not be found. + Τα στοιχεία πιστοποίησης δε βρέθηκαν. + + + Authentication request could not be processed due to a system problem. + Το αίτημα πιστοποίησης δε μπορεί να επεξεργαστεί λόγω σφάλματος του συστήματος. + + + Invalid credentials. + Λανθασμένα στοιχεία σύνδεσης. + + + Cookie has already been used by someone else. + Το Cookie έχει ήδη χρησιμοποιηθεί από κάποιον άλλο. + + + Not privileged to request the resource. + Δεν είστε εξουσιοδοτημένος για πρόσβαση στο συγκεκριμένο περιεχόμενο. + + + Invalid CSRF token. + Μη έγκυρο CSRF token. + + + No authentication provider found to support the authentication token. + Δε βρέθηκε κάποιος πάροχος πιστοποίησης που να υποστηρίζει το token πιστοποίησης. + + + No session available, it either timed out or cookies are not enabled. + Δεν υπάρχει ενεργή σύνοδος (session), είτε έχει λήξει ή τα cookies δεν είναι ενεργοποιημένα. + + + No token could be found. + Δεν ήταν δυνατόν να βρεθεί κάποιο token. + + + Username could not be found. + Το όνομα χρήστη δε βρέθηκε. + + + Account has expired. + Ο λογαριασμός έχει λήξει. + + + Credentials have expired. + Τα στοιχεία σύνδεσης έχουν λήξει. + + + Account is disabled. + Ο λογαριασμός είναι απενεργοποιημένος. + + + Account is locked. + Ο λογαριασμός είναι κλειδωμένος. + + + Too many failed login attempts, please try again later. + Πολλαπλές αποτυχημένες απόπειρες σύνδεσης, παρακαλούμε ξαναδοκιμάστε αργότερα. + + + Invalid or expired login link. + Μη έγκυρος ή ληγμένος σύνδεσμος σύνδεσης. + + + Too many failed login attempts, please try again in %minutes% minute. + Πολλαπλές αποτυχημένες απόπειρες σύνδεσης, παρακαλούμε ξαναδοκιμάστε σε %minutes% λεπτό. + + + Too many failed login attempts, please try again in %minutes% minutes. + Πολλές αποτυχημένες προσπάθειες σύνδεσης, δοκιμάστε ξανά σε %minutes% λεπτά. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.en.xlf b/lib/symfony/security-core/Resources/translations/security.en.xlf new file mode 100644 index 0000000000..dffde89751 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.en.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + An authentication exception occurred. + + + Authentication credentials could not be found. + Authentication credentials could not be found. + + + Authentication request could not be processed due to a system problem. + Authentication request could not be processed due to a system problem. + + + Invalid credentials. + Invalid credentials. + + + Cookie has already been used by someone else. + Cookie has already been used by someone else. + + + Not privileged to request the resource. + Not privileged to request the resource. + + + Invalid CSRF token. + Invalid CSRF token. + + + No authentication provider found to support the authentication token. + No authentication provider found to support the authentication token. + + + No session available, it either timed out or cookies are not enabled. + No session available, it either timed out or cookies are not enabled. + + + No token could be found. + No token could be found. + + + Username could not be found. + Username could not be found. + + + Account has expired. + Account has expired. + + + Credentials have expired. + Credentials have expired. + + + Account is disabled. + Account is disabled. + + + Account is locked. + Account is locked. + + + Too many failed login attempts, please try again later. + Too many failed login attempts, please try again later. + + + Invalid or expired login link. + Invalid or expired login link. + + + Too many failed login attempts, please try again in %minutes% minute. + Too many failed login attempts, please try again in %minutes% minute. + + + Too many failed login attempts, please try again in %minutes% minutes. + Too many failed login attempts, please try again in %minutes% minutes. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.es.xlf b/lib/symfony/security-core/Resources/translations/security.es.xlf new file mode 100644 index 0000000000..d1ebb1e343 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.es.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ocurrió un error de autenticación. + + + Authentication credentials could not be found. + No se encontraron las credenciales de autenticación. + + + Authentication request could not be processed due to a system problem. + La solicitud de autenticación no se pudo procesar debido a un problema del sistema. + + + Invalid credentials. + Credenciales no válidas. + + + Cookie has already been used by someone else. + La cookie ya ha sido usada por otra persona. + + + Not privileged to request the resource. + No tiene privilegios para solicitar el recurso. + + + Invalid CSRF token. + Token CSRF no válido. + + + No authentication provider found to support the authentication token. + No se encontró un proveedor de autenticación que soporte el token de autenticación. + + + No session available, it either timed out or cookies are not enabled. + No hay ninguna sesión disponible, ha expirado o las cookies no están habilitados. + + + No token could be found. + No se encontró ningún token. + + + Username could not be found. + No se encontró el nombre de usuario. + + + Account has expired. + La cuenta ha expirado. + + + Credentials have expired. + Las credenciales han expirado. + + + Account is disabled. + La cuenta está deshabilitada. + + + Account is locked. + La cuenta está bloqueada. + + + Too many failed login attempts, please try again later. + Demasiados intentos fallidos de inicio de sesión, inténtelo de nuevo más tarde. + + + Invalid or expired login link. + Enlace de inicio de sesión inválido o expirado. + + + Too many failed login attempts, please try again in %minutes% minute. + Demasiados intentos fallidos de inicio de sesión, inténtelo de nuevo en %minutes% minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Demasiados intentos fallidos de inicio de sesión, inténtelo de nuevo en %minutes% minutos. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.et.xlf b/lib/symfony/security-core/Resources/translations/security.et.xlf new file mode 100644 index 0000000000..b87cb71cee --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.et.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Autentimisel juhtus ootamatu viga. + + + Authentication credentials could not be found. + Autentimisandmeid ei leitud. + + + Authentication request could not be processed due to a system problem. + Autentimispäring ei õnnestunud süsteemi probleemi tõttu. + + + Invalid credentials. + Vigased autentimisandmed. + + + Cookie has already been used by someone else. + Küpsis on juba kellegi teise poolt kasutuses. + + + Not privileged to request the resource. + Ressursi pärimiseks pole piisavalt õiguseid. + + + Invalid CSRF token. + Vigane CSRF märgis. + + + No authentication provider found to support the authentication token. + Ei leitud sobivat autentimismeetodit, mis toetaks autentimismärgist. + + + No session available, it either timed out or cookies are not enabled. + Seanss puudub, see on kas aegunud või pole küpsised lubatud. + + + No token could be found. + Identsustõendit ei leitud. + + + Username could not be found. + Kasutajanime ei leitud. + + + Account has expired. + Kasutajakonto on aegunud. + + + Credentials have expired. + Autentimistunnused on aegunud. + + + Account is disabled. + Kasutajakonto on keelatud. + + + Account is locked. + Kasutajakonto on lukustatud. + + + Too many failed login attempts, please try again later. + Liiga palju ebaõnnestunud autentimise katseid, palun proovi hiljem uuesti. + + + Invalid or expired login link. + Vigane või aegunud sisselogimise link. + + + Too many failed login attempts, please try again in %minutes% minute. + Liiga palju ebaõnnestunud autentimise katseid, palun proovi uuesti %minutes% minuti pärast. + + + Too many failed login attempts, please try again in %minutes% minutes. + Liiga palju nurjunud sisselogimiskatseid, proovige uuesti %minutes% minuti pärast. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.eu.xlf b/lib/symfony/security-core/Resources/translations/security.eu.xlf new file mode 100644 index 0000000000..0f0a71342f --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.eu.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Autentifikazio-errorea gertatu da. + + + Authentication credentials could not be found. + Ez dira aurkitu autentifikazio-kredentzialak. + + + Authentication request could not be processed due to a system problem. + Ezin izan da autentifikazio-eskaera prozesatu, sistema-arazo bat gertatu da eta. + + + Invalid credentials. + Kredentzialak okerrak dira. + + + Cookie has already been used by someone else. + Dagoeneko beste pertsona batek erabili du cookiea. + + + Not privileged to request the resource. + Ez duzu baliabidea eskatzeko aukerarik. + + + Invalid CSRF token. + CSRF tokena okerra da. + + + No authentication provider found to support the authentication token. + Ez da aurkitu autentifikazio-tokena eutsi dezakeen autentifikazio-hornitzailerik. + + + No session available, it either timed out or cookies are not enabled. + Ez dago saiorik erabilgarri, iraungi egin da edo cookieak ez daude gaituta. + + + No token could be found. + Ez da tokenik aurkitu. + + + Username could not be found. + Ez da erabiltzaile-izena aurkitu. + + + Account has expired. + Kontua iraungi da. + + + Credentials have expired. + Kredentzialak iraungi dira. + + + Account is disabled. + Kontua desgaituta dago. + + + Account is locked. + Kontua blokeatuta dago. + + + Too many failed login attempts, please try again later. + Saioa hasteko saio huts gehiegi, saiatu berriro geroago. + + + Invalid or expired login link. + Sartzeko esteka baliogabea edo iraungia. + + + Too many failed login attempts, please try again in %minutes% minute. + Saioa hasteko huts gehiegi egin dira, saiatu berriro minutu %minutes% geroago. + + + Too many failed login attempts, please try again in %minutes% minutes. + Saioa hasteko saiakera huts gehiegi, saiatu berriro %minutes% minututan. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.fa.xlf b/lib/symfony/security-core/Resources/translations/security.fa.xlf new file mode 100644 index 0000000000..548fd35b2b --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.fa.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + خطایی هنگام احراز هویت رخ داده است. + + + Authentication credentials could not be found. + شرایط احراز هویت یافت نشد. + + + Authentication request could not be processed due to a system problem. + درخواست احراز هویت به دلیل وجود مشکل در سیستم قابل پردازش نمی باشد. + + + Invalid credentials. + احراز هویت نامعتبر می باشد. + + + Cookie has already been used by someone else. + Cookie قبلا توسط شخص دیگری استفاده گردیده است. + + + Not privileged to request the resource. + دسترسی لازم برای درخواست از این منبع را دارا نمی باشید. + + + Invalid CSRF token. + توکن CSRF معتبر نمی باشد. + + + No authentication provider found to support the authentication token. + هیچ ارائه دهنده احراز هویتی برای پشتیبانی از توکن احراز هویت پیدا نشد. + + + No session available, it either timed out or cookies are not enabled. + هیچ جلسه‌ای در دسترس نمی باشد. این میتواند به دلیل پایان یافتن زمان و یا فعال نبودن کوکی ها باشد. + + + No token could be found. + هیچ توکنی پیدا نشد. + + + Username could not be found. + نام ‌کاربری پیدا نشد. + + + Account has expired. + حساب کاربری منقضی گردیده است. + + + Credentials have expired. + مجوزهای احراز هویت منقضی گردیده‌اند. + + + Account is disabled. + حساب کاربری غیرفعال می باشد. + + + Account is locked. + حساب کاربری قفل گردیده است. + + + Too many failed login attempts, please try again later. + تلاش‌های ناموفق زیادی برای ورود صورت گرفته است، لطفاً بعداً دوباره امتحان کنید. + + + Invalid or expired login link. + لینک ورود نامعتبر یا تاریخ‌گذشته است. + + + Too many failed login attempts, please try again in %minutes% minute. + تلاش‌های ناموفق زیادی برای ورود صورت گرفته است، لطفاً %minutes% دقیقه دیگر دوباره امتحان کنید. + + + Too many failed login attempts, please try again in %minutes% minutes. + تعداد دفعات تلاش برای ورود بیش از حد زیاد است، لطفا پس از %minutes% دقیقه دوباره تلاش کنید. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.fi.xlf b/lib/symfony/security-core/Resources/translations/security.fi.xlf new file mode 100644 index 0000000000..7df4a19347 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.fi.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Autentikointi poikkeus tapahtui. + + + Authentication credentials could not be found. + Autentikoinnin tunnistetietoja ei löydetty. + + + Authentication request could not be processed due to a system problem. + Autentikointipyyntöä ei voitu käsitellä järjestelmäongelman vuoksi. + + + Invalid credentials. + Virheelliset tunnistetiedot. + + + Cookie has already been used by someone else. + Eväste on jo jonkin muun käytössä. + + + Not privileged to request the resource. + Ei oikeutta resurssiin. + + + Invalid CSRF token. + Virheellinen CSRF tunnus. + + + No authentication provider found to support the authentication token. + Autentikointi tunnukselle ei löydetty tuettua autentikointi tarjoajaa. + + + No session available, it either timed out or cookies are not enabled. + Sessio ei ole saatavilla, se on joko vanhentunut tai evästeet eivät ole käytössä. + + + No token could be found. + Tunnusta ei löytynyt. + + + Username could not be found. + Käyttäjätunnusta ei löydetty. + + + Account has expired. + Tili on vanhentunut. + + + Credentials have expired. + Tunnistetiedot ovat vanhentuneet. + + + Account is disabled. + Tili on poistettu käytöstä. + + + Account is locked. + Tili on lukittu. + + + Too many failed login attempts, please try again later. + Liian monta epäonnistunutta kirjautumisyritystä, yritä myöhemmin uudelleen. + + + Invalid or expired login link. + Virheellinen tai vanhentunut kirjautumislinkki. + + + Too many failed login attempts, please try again in %minutes% minute. + Liian monta epäonnistunutta kirjautumisyritystä, yritä uudelleen %minutes% minuutin kuluttua. + + + Too many failed login attempts, please try again in %minutes% minutes. + Liian monta epäonnistunutta kirjautumisyritystä, yritä uudelleen %minutes% minuutin kuluttua. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.fr.xlf b/lib/symfony/security-core/Resources/translations/security.fr.xlf new file mode 100644 index 0000000000..058ad9473b --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.fr.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Une exception d'authentification s'est produite. + + + Authentication credentials could not be found. + Les identifiants d'authentification n'ont pas pu être trouvés. + + + Authentication request could not be processed due to a system problem. + La requête d'authentification n'a pas pu être executée à cause d'un problème système. + + + Invalid credentials. + Identifiants invalides. + + + Cookie has already been used by someone else. + Le cookie a déjà été utilisé par quelqu'un d'autre. + + + Not privileged to request the resource. + Privilèges insuffisants pour accéder à la ressource. + + + Invalid CSRF token. + Jeton CSRF invalide. + + + No authentication provider found to support the authentication token. + Aucun fournisseur d'authentification n'a été trouvé pour supporter le jeton d'authentification. + + + No session available, it either timed out or cookies are not enabled. + Aucune session disponible, celle-ci a expiré ou les cookies ne sont pas activés. + + + No token could be found. + Aucun jeton n'a pu être trouvé. + + + Username could not be found. + Le nom d'utilisateur n'a pas pu être trouvé. + + + Account has expired. + Le compte a expiré. + + + Credentials have expired. + Les identifiants ont expiré. + + + Account is disabled. + Le compte est désactivé. + + + Account is locked. + Le compte est bloqué. + + + Too many failed login attempts, please try again later. + Plusieurs tentatives de connexion ont échoué, veuillez réessayer plus tard. + + + Invalid or expired login link. + Lien de connexion invalide ou expiré. + + + Too many failed login attempts, please try again in %minutes% minute. + Plusieurs tentatives de connexion ont échoué, veuillez réessayer dans %minutes% minute. + + + Too many failed login attempts, please try again in %minutes% minutes. + Trop de tentatives de connexion échouées, veuillez réessayer dans %minutes% minutes. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.gl.xlf b/lib/symfony/security-core/Resources/translations/security.gl.xlf new file mode 100644 index 0000000000..49f48dbed9 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.gl.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ocorreu un erro de autenticación. + + + Authentication credentials could not be found. + Non se atoparon as credenciais de autenticación. + + + Authentication request could not be processed due to a system problem. + A solicitude de autenticación no puido ser procesada debido a un problema do sistema. + + + Invalid credentials. + Credenciais non válidas. + + + Cookie has already been used by someone else. + A cookie xa foi empregado por outro usuario. + + + Not privileged to request the resource. + Non ten privilexios para solicitar o recurso. + + + Invalid CSRF token. + Token CSRF non válido. + + + No authentication provider found to support the authentication token. + Non se atopou un provedor de autenticación que soporte o token de autenticación. + + + No session available, it either timed out or cookies are not enabled. + Non hai ningunha sesión dispoñible, expirou ou as cookies non están habilitadas. + + + No token could be found. + Non se atopou ningún token. + + + Username could not be found. + Non se atopou o nome de usuario. + + + Account has expired. + A conta expirou. + + + Credentials have expired. + As credenciais expiraron. + + + Account is disabled. + A conta está deshabilitada. + + + Account is locked. + A conta está bloqueada. + + + Too many failed login attempts, please try again later. + Demasiados intentos de inicio de sesión fallados. Téntao de novo máis tarde. + + + Invalid or expired login link. + Ligazón de inicio de sesión non válida ou caducada. + + + Too many failed login attempts, please try again in %minutes% minute. + Demasiados intentos de inicio de sesión errados, por favor, ténteo de novo en %minutes% minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Demasiados intentos fallidos de inicio de sesión, inténtao de novo en %minutes% minutos. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.he.xlf b/lib/symfony/security-core/Resources/translations/security.he.xlf new file mode 100644 index 0000000000..1cf02a4ee7 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.he.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + התרחשה שגיאה באימות. + + + Authentication credentials could not be found. + פרטי הזיהוי לא נמצאו. + + + Authentication request could not be processed due to a system problem. + לא ניתן היה לעבד את בקשת האימות בגלל בעיית מערכת. + + + Invalid credentials. + שם משתמש או סיסמא שגויים. + + + Cookie has already been used by someone else. + עוגיה כבר שומשה על ידי מישהו אחר. + + + Not privileged to request the resource. + אין הרשאה מתאימה. + + + Invalid CSRF token. + אסימון CSRF לא חוקי. + + + No authentication provider found to support the authentication token. + לא נמצא ספק אימות המתאים לבקשה. + + + No session available, it either timed out or cookies are not enabled. + אין מפגש זמין, תם הזמן הקצוב או שהעוגיות אינן מופעלות. + + + No token could be found. + אסימון לא נמצא. + + + Username could not be found. + שם משתמש לא נמצא. + + + Account has expired. + החשבון פג תוקף. + + + Credentials have expired. + פרטי התחברות פקעו תוקף. + + + Account is disabled. + החשבון מבוטל. + + + Account is locked. + החשבון נעול. + + + Too many failed login attempts, please try again later. + יותר מדי ניסיונות כניסה כושלים, אנא נסה שוב מאוחר יותר. + + + Invalid or expired login link. + קישור כניסה לא חוקי או שפג תוקפו. + + + Too many failed login attempts, please try again in %minutes% minute. + יותר מדי ניסיונות כניסה כושלים, אנא נסה שוב בעוד %minutes% דקה. + + + Too many failed login attempts, please try again in %minutes% minutes. + יותר מדי ניסיונות כניסה כושלים, אנא נסה שוב בעוד %minutes% דקות. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.hr.xlf b/lib/symfony/security-core/Resources/translations/security.hr.xlf new file mode 100644 index 0000000000..f3b5a257e5 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.hr.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Dogodila se autentifikacijske iznimka. + + + Authentication credentials could not be found. + Autentifikacijski podaci nisu pronađeni. + + + Authentication request could not be processed due to a system problem. + Autentifikacijski zahtjev nije moguće provesti uslijed sistemskog problema. + + + Invalid credentials. + Neispravni akreditacijski podaci. + + + Cookie has already been used by someone else. + Cookie je već netko drugi iskoristio. + + + Not privileged to request the resource. + Nemate privilegije zahtijevati resurs. + + + Invalid CSRF token. + Neispravan CSRF token. + + + No authentication provider found to support the authentication token. + Nije pronađen autentifikacijski provider koji bi podržao autentifikacijski token. + + + No session available, it either timed out or cookies are not enabled. + Sesija nije dostupna, ili je istekla ili cookies nisu omogućeni. + + + No token could be found. + Token nije pronađen. + + + Username could not be found. + Korisničko ime nije pronađeno. + + + Account has expired. + Račun je isteko. + + + Credentials have expired. + Akreditacijski podaci su istekli. + + + Account is disabled. + Račun je onemogućen. + + + Account is locked. + Račun je zaključan. + + + Too many failed login attempts, please try again later. + Previše neuspjelih pokušaja prijave, molim pokušajte ponovo kasnije. + + + Invalid or expired login link. + Link za prijavu je isteako ili je neispravan. + + + Too many failed login attempts, please try again in %minutes% minute. + Previše neuspjelih pokušaja prijave, molim pokušajte ponovo za %minutes% minutu. + + + Too many failed login attempts, please try again in %minutes% minutes. + Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minutu.|Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minute.|Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minuta. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.hu.xlf b/lib/symfony/security-core/Resources/translations/security.hu.xlf new file mode 100644 index 0000000000..06096dc4a2 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.hu.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Hitelesítési hiba lépett fel. + + + Authentication credentials could not be found. + Nem találhatók hitelesítési információk. + + + Authentication request could not be processed due to a system problem. + A hitelesítési kérést rendszerhiba miatt nem lehet feldolgozni. + + + Invalid credentials. + Érvénytelen hitelesítési információk. + + + Cookie has already been used by someone else. + Ezt a sütit valaki más már felhasználta. + + + Not privileged to request the resource. + Nem rendelkezik az erőforrás eléréséhez szükséges jogosultsággal. + + + Invalid CSRF token. + Érvénytelen CSRF token. + + + No authentication provider found to support the authentication token. + Nem található a hitelesítési tokent támogató hitelesítési szolgáltatás. + + + No session available, it either timed out or cookies are not enabled. + Munkamenet nem áll rendelkezésre, túllépte az időkeretet vagy a sütik le vannak tiltva. + + + No token could be found. + Nem található token. + + + Username could not be found. + A felhasználónév nem található. + + + Account has expired. + A fiók lejárt. + + + Credentials have expired. + A hitelesítési információk lejártak. + + + Account is disabled. + Felfüggesztett fiók. + + + Account is locked. + Zárolt fiók. + + + Too many failed login attempts, please try again later. + Túl sok sikertelen bejelentkezési kísérlet, kérjük próbálja újra később. + + + Invalid or expired login link. + Érvénytelen vagy lejárt bejelentkezési link. + + + Too many failed login attempts, please try again in %minutes% minute. + Túl sok sikertelen bejelentkezési kísérlet, kérjük próbálja újra %minutes% perc múlva. + + + Too many failed login attempts, please try again in %minutes% minutes. + Túl sok sikertelen bejelentkezési kísérlet, kérjük, próbálja újra %minutes% perc múlva. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.hy.xlf b/lib/symfony/security-core/Resources/translations/security.hy.xlf new file mode 100644 index 0000000000..e506c91988 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.hy.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Նույնականացման սխալ։ + + + Authentication credentials could not be found. + Նույնականացման տվյալները չեն գտնվել։ + + + Authentication request could not be processed due to a system problem. + Համակարգային սխալ՝ նույնականացման հացրման պրոցեսինգի ժամանակ։ + + + Invalid credentials. + Սխալ մուտքային տվյալներ + + + Cookie has already been used by someone else. + Cookie-ն արդեն օգտագործվում է ուրիշի կողմից։ + + + Not privileged to request the resource. + Ռեսուրսի հարցման համար չկա թույլատվություն։ + + + Invalid CSRF token. + Անվավեր CSRF թոքեն։ + + + No authentication provider found to support the authentication token. + Նույնականացման ոչ մի մատակարար չի գտնվել, որ աջակցի նույնականացման թոքենը։ + + + No session available, it either timed out or cookies are not enabled. + Հասանելի սեսիա չկա, կամ այն սպառվել է կամ cookie-ները անջատված են: + + + No token could be found. + Թոքենը չի գտնվել։ + + + Username could not be found. + Օգտանունը չի գտնվել։ + + + Account has expired. + Հաշիվը ժամկետանց է։ + + + Credentials have expired. + Մուտքային տվյալները ժամկետանց են։ + + + Account is disabled. + Հաշիվը դեկատիվացված է։ + + + Account is locked. + Հաշիվն արգելափակված է։ + + + Too many failed login attempts, please try again later. + Չափից շատ մուտքի փորձեր, խնդրում ենք փորձել մի փոքր ուշ + + + Invalid or expired login link. + Անվավեր կամ ժամկետանց մուտքի հղում։ + + + Too many failed login attempts, please try again in %minutes% minute. + Մուտքի չափազանց շատ անհաջող փորձեր: Խնդրում ենք կրկին փորձել %minutes րոպե: + + + Too many failed login attempts, please try again in %minutes% minutes. + Չափազանց շատ անհաջող մուտքի փորձեր, խնդրում ենք փորձել կրկին %minutes% րոպեից. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.id.xlf b/lib/symfony/security-core/Resources/translations/security.id.xlf new file mode 100644 index 0000000000..4c1cd9965e --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.id.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Terjadi kesalahan otentikasi. + + + Authentication credentials could not be found. + Kredensial otentikasi tidak bisa ditemukan. + + + Authentication request could not be processed due to a system problem. + Permintaan otentikasi tidak bisa diproses karena masalah sistem. + + + Invalid credentials. + Kredensial tidak valid. + + + Cookie has already been used by someone else. + Cookie sudah digunakan oleh orang lain. + + + Not privileged to request the resource. + Tidak berhak untuk meminta sumber daya. + + + Invalid CSRF token. + Token CSRF tidak valid. + + + No authentication provider found to support the authentication token. + Tidak ditemukan penyedia otentikasi untuk mendukung token otentikasi. + + + No session available, it either timed out or cookies are not enabled. + Tidak ada sesi yang tersedia, mungkin waktu sudah habis atau cookie tidak diaktifkan + + + No token could be found. + Tidak ada token yang bisa ditemukan. + + + Username could not be found. + Username tidak bisa ditemukan. + + + Account has expired. + Akun telah berakhir. + + + Credentials have expired. + Kredensial telah berakhir. + + + Account is disabled. + Akun dinonaktifkan. + + + Account is locked. + Akun terkunci. + + + Too many failed login attempts, please try again later. + Terlalu banyak percobaan login yang gagal, silahkan coba lagi nanti. + + + Invalid or expired login link. + Link login tidak valid atau sudah kedaluwarsa. + + + Too many failed login attempts, please try again in %minutes% minute. + Terlalu banyak percobaan login yang gagal, silahkan coba lagi dalam %minutes% menit. + + + Too many failed login attempts, please try again in %minutes% minutes. + Terlalu banyak upaya login yang gagal, silakan coba lagi dalam beberapa %minutes% menit. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.it.xlf b/lib/symfony/security-core/Resources/translations/security.it.xlf new file mode 100644 index 0000000000..72eace25e8 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.it.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Si è verificato un errore di autenticazione. + + + Authentication credentials could not be found. + Impossibile trovare le credenziali di autenticazione. + + + Authentication request could not be processed due to a system problem. + La richiesta di autenticazione non può essere processata a causa di un errore di sistema. + + + Invalid credentials. + Credenziali non valide. + + + Cookie has already been used by someone else. + Il cookie è già stato usato da qualcun altro. + + + Not privileged to request the resource. + Non hai i privilegi per richiedere questa risorsa. + + + Invalid CSRF token. + CSRF token non valido. + + + No authentication provider found to support the authentication token. + Non è stato trovato un valido fornitore di autenticazione per supportare il token. + + + No session available, it either timed out or cookies are not enabled. + Nessuna sessione disponibile, può essere scaduta o i cookie non sono abilitati. + + + No token could be found. + Nessun token trovato. + + + Username could not be found. + Username non trovato. + + + Account has expired. + Account scaduto. + + + Credentials have expired. + Credenziali scadute. + + + Account is disabled. + L'account è disabilitato. + + + Account is locked. + L'account è bloccato. + + + Too many failed login attempts, please try again later. + Troppi tentativi di login falliti, riprova tra un po'. + + + Invalid or expired login link. + Link di login scaduto o non valido. + + + Too many failed login attempts, please try again in %minutes% minute. + Troppi tentativi di login falliti, riprova tra %minutes% minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Troppi tentativi di login falliti, riprova tra %minutes% minuti. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.ja.xlf b/lib/symfony/security-core/Resources/translations/security.ja.xlf new file mode 100644 index 0000000000..bc3a18aefd --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.ja.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + 認証エラーが発生しました。 + + + Authentication credentials could not be found. + 認証資格がありません。 + + + Authentication request could not be processed due to a system problem. + システムの問題により認証要求を処理できませんでした。 + + + Invalid credentials. + 資格が無効です。 + + + Cookie has already been used by someone else. + Cookie が別のユーザーで使用されています。 + + + Not privileged to request the resource. + リソースをリクエストする権限がありません。 + + + Invalid CSRF token. + CSRF トークンが無効です。 + + + No authentication provider found to support the authentication token. + 認証トークンをサポートする認証プロバイダーが見つかりません。 + + + No session available, it either timed out or cookies are not enabled. + 利用可能なセッションがありません。タイムアウトしたか、Cookie が無効になっています。 + + + No token could be found. + トークンが見つかりません。 + + + Username could not be found. + ユーザー名が見つかりません。 + + + Account has expired. + アカウントが有効期限切れです。 + + + Credentials have expired. + 資格が有効期限切れです。 + + + Account is disabled. + アカウントが無効です。 + + + Account is locked. + アカウントはロックされています。 + + + Too many failed login attempts, please try again later. + ログイン試行回数を超えました。しばらくして再度お試しください。 + + + Invalid or expired login link. + ログインリンクが有効期限切れ、もしくは無効です。 + + + Too many failed login attempts, please try again in %minutes% minute. + ログイン試行回数が多すぎます。%minutes%分後に再度お試しください。 + + + Too many failed login attempts, please try again in %minutes% minutes. + ログイン試行回数が多すぎます。%minutes%分後に再度お試しください。 + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.lb.xlf b/lib/symfony/security-core/Resources/translations/security.lb.xlf new file mode 100644 index 0000000000..181ef2444f --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.lb.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Bei der Authentifikatioun ass e Feeler opgetrueden. + + + Authentication credentials could not be found. + Et konnte keng Zouganksdate fonnt ginn. + + + Authentication request could not be processed due to a system problem. + D'Ufro fir eng Authentifikatioun konnt wéinst engem Problem vum System net beaarbecht ginn. + + + Invalid credentials. + Ongëlteg Zouganksdaten. + + + Cookie has already been used by someone else. + De Cookie gouf scho vun engem anere benotzt. + + + Not privileged to request the resource. + Keng Rechter fir d'Ressource unzefroen. + + + Invalid CSRF token. + Ongëltegen CSRF-Token. + + + No authentication provider found to support the authentication token. + Et gouf keen Authentifizéierungs-Provider fonnt deen den Authentifizéierungs-Token ënnerstëtzt. + + + No session available, it either timed out or cookies are not enabled. + Keng Sëtzung disponibel. Entweder ass se ofgelaf oder Cookies sinn net aktivéiert. + + + No token could be found. + Et konnt keen Token fonnt ginn. + + + Username could not be found. + De Benotzernumm konnt net fonnt ginn. + + + Account has expired. + Den Account ass ofgelaf. + + + Credentials have expired. + D'Zouganksdate sinn ofgelaf. + + + Account is disabled. + De Konto ass deaktivéiert. + + + Account is locked. + De Konto ass gespaart. + + + Too many failed login attempts, please try again later. + Ze vill mësslonge Login-Versich, w.e.g. méi spéit nach emol probéieren. + + + Invalid or expired login link. + Ongëltegen oder ofgelafene Login-Link. + + + Too many failed login attempts, please try again in %minutes% minute. + Zu vill fehlgeschloen Loginversich, w. e. g. probéiert nach am %minutes% Minutt. + + + Too many failed login attempts, please try again in %minutes% minutes. + Ze vill Feeler beim Umellen, versicht weg erëm an %minutes% Minutten. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.lt.xlf b/lib/symfony/security-core/Resources/translations/security.lt.xlf new file mode 100644 index 0000000000..8053d0da23 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.lt.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Įvyko autentifikacijos klaida. + + + Authentication credentials could not be found. + Nepavyko rasti autentifikacijos duomenų. + + + Authentication request could not be processed due to a system problem. + Autentifikacijos užklausos nepavyko įvykdyti dėl sistemos klaidų. + + + Invalid credentials. + Klaidingi duomenys. + + + Cookie has already been used by someone else. + Slapukas buvo panaudotas kažkam kitam. + + + Not privileged to request the resource. + Neturite teisių pasiektį resursą. + + + Invalid CSRF token. + Neteisingas CSRF raktas. + + + No authentication provider found to support the authentication token. + Nerastas autentifikacijos tiekėjas, kuris palaikytų autentifikacijos raktą. + + + No session available, it either timed out or cookies are not enabled. + Sesija yra nepasiekiama, pasibaigė galiojimo laikas arba slapukai yra išjungti. + + + No token could be found. + Nepavyko rasti rakto. + + + Username could not be found. + Tokio naudotojo vardo nepavyko rasti. + + + Account has expired. + Paskyros galiojimo laikas baigėsi. + + + Credentials have expired. + Autentifikacijos duomenų galiojimo laikas baigėsi. + + + Account is disabled. + Paskyra yra išjungta. + + + Account is locked. + Paskyra yra užblokuota. + + + Too many failed login attempts, please try again later. + Per daug nepavykusių prisijungimo bandymų, pabandykite dar kartą vėliau. + + + Invalid or expired login link. + Netinkama arba pasibaigusio galiojimo laiko prisijungimo nuoroda. + + + Too many failed login attempts, please try again in %minutes% minute. + Per daug nepavykusių prisijungimo bandymų, pabandykite dar kartą po %minutes% minutės. + + + Too many failed login attempts, please try again in %minutes% minutes. + Per daug nesėkmingų prisijungimo bandymų, bandykite vėl po %minutes% minutės.|Per daug nesėkmingų prisijungimo bandymų, bandykite vėl po %minutes% minučių. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.lv.xlf b/lib/symfony/security-core/Resources/translations/security.lv.xlf new file mode 100644 index 0000000000..c431ed4046 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.lv.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Radās autentifikācijas kļūda. + + + Authentication credentials could not be found. + Autentifikācijas dati nav atrasti. + + + Authentication request could not be processed due to a system problem. + Autentifikācijas pieprasījums nevar tikt apstrādāts sistēmas problēmas dēļ. + + + Invalid credentials. + Nederīgi autentifikācijas dati. + + + Cookie has already been used by someone else. + Kāds cits jau izmantoja sīkdatni. + + + Not privileged to request the resource. + Nav tiesību šī resursa izsaukšanai. + + + Invalid CSRF token. + Nederīgs CSRF talons. + + + No authentication provider found to support the authentication token. + Nav atrasts, autentifikācijas talonu atbalstošs, autentifikācijas sniedzējs. + + + No session available, it either timed out or cookies are not enabled. + Sesija nav pieejama - vai nu tā beidzās, vai nu sīkdatnes nav iespējotas. + + + No token could be found. + Nevar atrast nevienu talonu. + + + Username could not be found. + Nevar atrast lietotājvārdu. + + + Account has expired. + Konta derīguma termiņš ir beidzies. + + + Credentials have expired. + Autentifikācijas datu derīguma termiņš ir beidzies. + + + Account is disabled. + Konts ir atspējots. + + + Account is locked. + Konts ir slēgts. + + + Too many failed login attempts, please try again later. + Pārāk daudz atteiktu autentifikācijas mēģinājumu, lūdzu, mēģiniet vēlreiz vēlāk. + + + Invalid or expired login link. + Autentifikācijas saite ir nederīga vai arī tai ir beidzies derīguma termiņš. + + + Too many failed login attempts, please try again in %minutes% minute. + Pārāk daudz nesekmīgu autentifikācijas mēģinājumu, lūdzu mēģiniet vēlreiz pēc %minutes% minūtes. + + + Too many failed login attempts, please try again in %minutes% minutes. + Pārāk daudz neveiksmīgu autentifikācijas mēģinājumu, lūdzu, mēģiniet vēlreiz pēc %minutes% minūtēm. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.mk.xlf b/lib/symfony/security-core/Resources/translations/security.mk.xlf new file mode 100644 index 0000000000..ba046eca2c --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.mk.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Настана грешка во автентикацијата. + + + Authentication credentials could not be found. + Акредитивите за автентикација не се пронајдени. + + + Authentication request could not be processed due to a system problem. + Барањето за автентикација не можеше да биде процесуирано заради системски проблем. + + + Invalid credentials. + Невалидни акредитиви. + + + Cookie has already been used by someone else. + Колачето е веќе користено од некој друг. + + + Not privileged to request the resource. + Немате привилегии за да го побарате ресурсот. + + + Invalid CSRF token. + Невалиден CSRF токен. + + + No authentication provider found to support the authentication token. + Не е пронајден провајдер за автентикација кој го поддржува токенот за автентикација. + + + No session available, it either timed out or cookies are not enabled. + Сесијата е недостапна, или е истечена, или колачињата не се овозможени. + + + No token could be found. + Токенот не е најден. + + + Username could not be found. + Корисничкото име не е најдено. + + + Account has expired. + Корисничката сметка е истечена. + + + Credentials have expired. + Акредитивите се истечени. + + + Account is disabled. + Корисничката сметка е деактивирана. + + + Account is locked. + Корисничката сметка е заклучена. + + + Too many failed login attempts, please try again later. + Премногу неуспешни обиди за најавување, ве молиме обидете се повторно подоцна. + + + Invalid or expired login link. + Неважечка или истечена врска за најавување. + + + Too many failed login attempts, please try again in %minutes% minute. + Премногу неуспешни обиди за најавување, обидете се повторно за %minutes% минута. + + + Too many failed login attempts, please try again in %minutes% minutes. + Претерано многу неуспешни обиди за најавување, ве молиме обидете се повторно за %minutes% минута.|Претерано многу неуспешни обиди за најавување, ве молиме обидете се повторно за %minutes% минути. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.mn.xlf b/lib/symfony/security-core/Resources/translations/security.mn.xlf new file mode 100644 index 0000000000..33a9ffda21 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.mn.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Нэвтрэх хүсэлтийн алдаа гарав. + + + Authentication credentials could not be found. + Нэвтрэх эрхийн мэдээлэл олдсонгүй. + + + Authentication request could not be processed due to a system problem. + Системийн алдаанаас болон нэвтрэх хүсэлтийг гүйцэтгэх боломжгүй байна. + + + Invalid credentials. + Буруу нэвтрэх эрхийн мэдээлэл. + + + Cookie has already been used by someone else. + Күүки файлыг аль хэдийн өөр хүн хэрэглэж байна. + + + Not privileged to request the resource. + Энэхүү мэдээллийг авах эрх хүрэхгүй байна. + + + Invalid CSRF token. + Тохиромжгүй CSRF токен. + + + No authentication provider found to support the authentication token. + Нэвтрэх токенг дэмжих нэвтрэх эрхийн хангагч олдсонгүй. + + + No session available, it either timed out or cookies are not enabled. + Хэрэглэгчийн session олдсонгүй, хугацаа нь дууссан эсвэл күүки идэвхижүүлээгүй байна. + + + No token could be found. + Токен олдсонгүй. + + + Username could not be found. + Нэвтрэх нэр олсонгүй. + + + Account has expired. + Бүртгэлийн хугацаа дууссан байна. + + + Credentials have expired. + Нэвтрэх эрхийн хугацаа дууссан байна. + + + Account is disabled. + Бүртгэлийг хаасан байна. + + + Account is locked. + Бүртгэлийг цоожилсон байна. + + + Too many failed login attempts, please try again later. + Хэтэрхий олон амжилтгүй оролдлого, түр хүлээгээд дахин оролдоно уу. + + + Invalid or expired login link. + Буруу эсвэл хугацаа нь дууссан нэвтрэх зам. + + + Too many failed login attempts, please try again in %minutes% minute. + Нэвтрэх оролдлого ихээр амжилтгүй болсон, %minutes% минутын дараа дахин оролдоно уу. + + + Too many failed login attempts, please try again in %minutes% minutes. + Хэт олон бүтэлгүй нэвтрэх оролдлого, %minutes% минутын дараа дахин оролдоно уу. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.my.xlf b/lib/symfony/security-core/Resources/translations/security.my.xlf new file mode 100644 index 0000000000..8550e745ef --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.my.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + အသုံးပြုခွင့် ခြွင်းချက်တစ်ခုဖြစ်သွားသည်။ + + + Authentication credentials could not be found. + အသုံးပြုခွင့် အထောက်အထားများ ရှာမတွေ့ပါ။ + + + Authentication request could not be processed due to a system problem. + System ပြဿနာအခက်အခဲရှိ နေပါသဖြင့် အသုံးပြုခွင့်တောင်းဆိုချက်ကို ဆောင်ရွက်၍မရ နိုင်ပါ။ + + + Invalid credentials. + သင့်လျှော်သော် အထောက်အထားမဟုတ်ပါ။ + + + Cookie has already been used by someone else. + Cookie ကို တစ်စုံတစ်ယောက်မှ အသုံးပြုပြီးဖြစ်သည်။ + + + Not privileged to request the resource. + အရင်းအမြစ်ကိုတောင်းဆိုရန်အခွင့်ထူးမရပါ။ + + + Invalid CSRF token. + သင့်လျှော်သော် CSRF token မဟုတ်ပါ။ + + + No authentication provider found to support the authentication token. + အထောက်အထားစိစစ်ခြင်းသင်္ကေတကိုပံ့ပိုးရန် မည်သည့်အထောက်အထားစိစစ်ရေး ၀န်ဆောင်မှုမှမတွေ့ပါ။ + + + No session available, it either timed out or cookies are not enabled. + Session မအားလပ်ပါ။ Session အချိန်ကုန်သွားခြင်း (သို့မဟုတ်) cookies များကိုဖွင့်ထားခြင်းမရှိပါ။ + + + No token could be found. + Toke ရှာမတွေ့ပါ။ + + + Username could not be found. + အသုံးပြုသူအမည် ရှာဖွေတွေ့ရှိချင်းမရှိပါ။ + + + Account has expired. + အကောင့် သက်တမ်းကုန်လွန်သွားပါပြီ။ + + + Credentials have expired. + အထောက်အထားသက်တန်း ကုန်လွန်သွားပါပြီ။ + + + Account is disabled. + အကောင့်ပိတ်ထားပါသည်။ + + + Account is locked. + အကောင့် လောခ်ကျသွားပါပြီ။ + + + Too many failed login attempts, please try again later. + Login ၀င်ရန်ကြိုးစားမှုများလွန်းပါသည်၊ ကျေးဇူးပြု၍ နောက်မှထပ်ကြိုးစားပါ။ + + + Invalid or expired login link. + မသင့်လျှော်သော် (သို့မဟုတ်) သက်တန်းကုန်သော login link ဖြစ်ပါသည်။ + + + Too many failed login attempts, please try again in %minutes% minute. + Login ၀င်ရန်ကြိုးစားမှုများလွန်းပါသည်၊ ကျေးဇူးပြု၍ နောက် %minutes% မှထပ်မံကြိုးစားပါ။ + + + Too many failed login attempts, please try again in %minutes% minutes. + ဝင်ရောက်မှု မအောင်မြင်သော ကြိုးပမ်းမှုများအတွက် တစ်ခါတည်း ပြန်လုပ်မည်။ ထပ်မံကြိုးစားကြည့်ပါ။ %minutes% မိနစ်အတွင်း|ဝင်ရောက်မှု မအောင်မြင်သော ကြိုးပမ်းမှုများအတွက် တစ်ခါတည်း ပြန်လုပ်မည်။ ထပ်မံကြိုးစားကြည့်ပါ။ %minutes% မိနစ်အတွင်း + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.nb.xlf b/lib/symfony/security-core/Resources/translations/security.nb.xlf new file mode 100644 index 0000000000..9ace014112 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.nb.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + En autentiseringsfeil har skjedd. + + + Authentication credentials could not be found. + Påloggingsinformasjonen kunne ikke bli funnet. + + + Authentication request could not be processed due to a system problem. + Autentiserings forespørselen kunne ikke bli prosessert grunnet en system feil. + + + Invalid credentials. + Ugyldig påloggingsinformasjon. + + + Cookie has already been used by someone else. + Cookie har allerede blitt brukt av noen andre. + + + Not privileged to request the resource. + Ingen tilgang til å be om gitt ressurs. + + + Invalid CSRF token. + Ugyldig CSRF token. + + + No authentication provider found to support the authentication token. + Ingen autentiserings tilbyder funnet som støtter gitt autentiserings token. + + + No session available, it either timed out or cookies are not enabled. + Ingen sesjon tilgjengelig, sesjonen er enten utløpt eller cookies ikke skrudd på. + + + No token could be found. + Ingen token kunne bli funnet. + + + Username could not be found. + Brukernavn kunne ikke bli funnet. + + + Account has expired. + Brukerkonto har utgått. + + + Credentials have expired. + Påloggingsinformasjon har utløpt. + + + Account is disabled. + Brukerkonto er deaktivert. + + + Account is locked. + Brukerkonto er sperret. + + + Too many failed login attempts, please try again later. + For mange mislykkede påloggingsforsøk. Prøv igjen senere. + + + Invalid or expired login link. + Ugyldig eller utløpt påloggingskobling. + + + Too many failed login attempts, please try again in %minutes% minute. + For mange mislykkede påloggingsforsøk, prøv igjen om %minutes% minutt. + + + Too many failed login attempts, please try again in %minutes% minutes. + For mange mislykkede påloggingsforsøk, prøv igjen om %minutes% minutter. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.nl.xlf b/lib/symfony/security-core/Resources/translations/security.nl.xlf new file mode 100644 index 0000000000..49b7aa78db --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.nl.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Er heeft zich een authenticatieprobleem voorgedaan. + + + Authentication credentials could not be found. + Authenticatiegegevens konden niet worden gevonden. + + + Authentication request could not be processed due to a system problem. + Authenticatieaanvraag kon niet worden verwerkt door een technisch probleem. + + + Invalid credentials. + Ongeldige inloggegevens. + + + Cookie has already been used by someone else. + Cookie is al door een ander persoon gebruikt. + + + Not privileged to request the resource. + Onvoldoende rechten om de aanvraag te verwerken. + + + Invalid CSRF token. + CSRF-code is ongeldig. + + + No authentication provider found to support the authentication token. + Geen authenticatieprovider gevonden die de authenticatietoken ondersteunt. + + + No session available, it either timed out or cookies are not enabled. + Geen sessie beschikbaar, mogelijk is deze verlopen of cookies zijn uitgeschakeld. + + + No token could be found. + Er kon geen authenticatietoken worden gevonden. + + + Username could not be found. + Gebruikersnaam kon niet worden gevonden. + + + Account has expired. + Account is verlopen. + + + Credentials have expired. + Authenticatiegegevens zijn verlopen. + + + Account is disabled. + Account is gedeactiveerd. + + + Account is locked. + Account is geblokkeerd. + + + Too many failed login attempts, please try again later. + Te veel onjuiste inlogpogingen, probeer het later nogmaals. + + + Invalid or expired login link. + Ongeldige of verlopen inloglink. + + + Too many failed login attempts, please try again in %minutes% minute. + Te veel onjuiste inlogpogingen, probeer het opnieuw over %minutes% minuut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Te veel onjuiste inlogpogingen, probeer het opnieuw over %minutes% minuten. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.nn.xlf b/lib/symfony/security-core/Resources/translations/security.nn.xlf new file mode 100644 index 0000000000..1a4c32b737 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.nn.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Innlogginga har feila. + + + Authentication credentials could not be found. + Innloggingsinformasjonen vart ikkje funnen. + + + Authentication request could not be processed due to a system problem. + Innlogginga vart ikkje fullført på grunn av ein systemfeil. + + + Invalid credentials. + Ugyldig innloggingsinformasjon. + + + Cookie has already been used by someone else. + Informasjonskapselen er allereie brukt av ein annan brukar. + + + Not privileged to request the resource. + Du har ikkje åtgang til å be om denne ressursen. + + + Invalid CSRF token. + Ugyldig CSRF-teikn. + + + No authentication provider found to support the authentication token. + Fann ingen innloggingstilbydar som støttar dette innloggingsteiknet. + + + No session available, it either timed out or cookies are not enabled. + Ingen sesjon tilgjengeleg. Sesjonen er anten ikkje lenger gyldig, eller informasjonskapslar er ikkje skrudd på i nettlesaren. + + + No token could be found. + Fann ingen innloggingsteikn. + + + Username could not be found. + Fann ikkje brukarnamnet. + + + Account has expired. + Brukarkontoen er utgjengen. + + + Credentials have expired. + Innloggingsinformasjonen er utgjengen. + + + Account is disabled. + Brukarkontoen er sperra. + + + Account is locked. + Brukarkontoen er sperra. + + + Too many failed login attempts, please try again later. + For mange innloggingsforsøk har feila, prøv igjen seinare. + + + Invalid or expired login link. + Innloggingslenka er ugyldig eller utgjengen. + + + Too many failed login attempts, please try again in %minutes% minute. + For mange mislykkede påloggingsforsøk, prøv igjen om %minutes% minutt. + + + Too many failed login attempts, please try again in %minutes% minutes. + For mange mislukka innloggingsforsøk, prøv igjen om %minutes% minutt. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.no.xlf b/lib/symfony/security-core/Resources/translations/security.no.xlf new file mode 100644 index 0000000000..9ace014112 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.no.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + En autentiseringsfeil har skjedd. + + + Authentication credentials could not be found. + Påloggingsinformasjonen kunne ikke bli funnet. + + + Authentication request could not be processed due to a system problem. + Autentiserings forespørselen kunne ikke bli prosessert grunnet en system feil. + + + Invalid credentials. + Ugyldig påloggingsinformasjon. + + + Cookie has already been used by someone else. + Cookie har allerede blitt brukt av noen andre. + + + Not privileged to request the resource. + Ingen tilgang til å be om gitt ressurs. + + + Invalid CSRF token. + Ugyldig CSRF token. + + + No authentication provider found to support the authentication token. + Ingen autentiserings tilbyder funnet som støtter gitt autentiserings token. + + + No session available, it either timed out or cookies are not enabled. + Ingen sesjon tilgjengelig, sesjonen er enten utløpt eller cookies ikke skrudd på. + + + No token could be found. + Ingen token kunne bli funnet. + + + Username could not be found. + Brukernavn kunne ikke bli funnet. + + + Account has expired. + Brukerkonto har utgått. + + + Credentials have expired. + Påloggingsinformasjon har utløpt. + + + Account is disabled. + Brukerkonto er deaktivert. + + + Account is locked. + Brukerkonto er sperret. + + + Too many failed login attempts, please try again later. + For mange mislykkede påloggingsforsøk. Prøv igjen senere. + + + Invalid or expired login link. + Ugyldig eller utløpt påloggingskobling. + + + Too many failed login attempts, please try again in %minutes% minute. + For mange mislykkede påloggingsforsøk, prøv igjen om %minutes% minutt. + + + Too many failed login attempts, please try again in %minutes% minutes. + For mange mislykkede påloggingsforsøk, prøv igjen om %minutes% minutter. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.pl.xlf b/lib/symfony/security-core/Resources/translations/security.pl.xlf new file mode 100644 index 0000000000..0cfc58b35b --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.pl.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Wystąpił błąd uwierzytelniania. + + + Authentication credentials could not be found. + Dane uwierzytelniania nie zostały znalezione. + + + Authentication request could not be processed due to a system problem. + Żądanie uwierzytelniania nie mogło zostać pomyślnie zakończone z powodu problemu z systemem. + + + Invalid credentials. + Nieprawidłowe dane. + + + Cookie has already been used by someone else. + To ciasteczko jest używane przez kogoś innego. + + + Not privileged to request the resource. + Brak uprawnień dla żądania wskazanego zasobu. + + + Invalid CSRF token. + Nieprawidłowy token CSRF. + + + No authentication provider found to support the authentication token. + Nie znaleziono mechanizmu uwierzytelniania zdolnego do obsługi przesłanego tokenu. + + + No session available, it either timed out or cookies are not enabled. + Brak danych sesji, sesja wygasła lub ciasteczka nie są włączone. + + + No token could be found. + Nie znaleziono tokenu. + + + Username could not be found. + Użytkownik o podanej nazwie nie istnieje. + + + Account has expired. + Konto wygasło. + + + Credentials have expired. + Dane uwierzytelniania wygasły. + + + Account is disabled. + Konto jest wyłączone. + + + Account is locked. + Konto jest zablokowane. + + + Too many failed login attempts, please try again later. + Zbyt dużo nieudanych prób logowania, proszę spróbować ponownie później. + + + Invalid or expired login link. + Nieprawidłowy lub wygasły link logowania. + + + Too many failed login attempts, please try again in %minutes% minute. + Zbyt wiele nieudanych prób logowania, spróbuj ponownie po upływie %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minutę.|Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minuty.|Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minut. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.pt.xlf b/lib/symfony/security-core/Resources/translations/security.pt.xlf new file mode 100644 index 0000000000..f9fda8d7b0 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.pt.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ocorreu uma exceção durante a autenticação. + + + Authentication credentials could not be found. + As credenciais de autenticação não foram encontradas. + + + Authentication request could not be processed due to a system problem. + A autenticação não foi concluída devido a um problema no sistema. + + + Invalid credentials. + Credenciais inválidas. + + + Cookie has already been used by someone else. + Este cookie já está em uso. + + + Not privileged to request the resource. + Sem privilégios para solicitar este recurso. + + + Invalid CSRF token. + Token CSRF inválido. + + + No authentication provider found to support the authentication token. + Nenhum fornecedor de autenticação encontrado para suportar o token de autenticação. + + + No session available, it either timed out or cookies are not enabled. + Nenhuma sessão disponível, esta expirou ou os cookies estão desativados. + + + No token could be found. + O token não foi encontrado. + + + Username could not be found. + Nome de usuário não encontrado. + + + Account has expired. + A conta expirou. + + + Credentials have expired. + As credenciais expiraram. + + + Account is disabled. + Conta desativada. + + + Account is locked. + A conta está bloqueada. + + + Too many failed login attempts, please try again later. + Muitas tentativas de login sem sucesso, por favor, tente mais tarde. + + + Invalid or expired login link. + Ligação de login inválida ou expirada. + + + Too many failed login attempts, please try again in %minutes% minute. + Muitas tentativas de login sem sucesso, por favor, tente novamente novamente em 1 minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Muitas tentativas de login sem sucesso, por favor, tente novamente em %minutes% minutos. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.pt_BR.xlf b/lib/symfony/security-core/Resources/translations/security.pt_BR.xlf new file mode 100644 index 0000000000..e3d7631db1 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.pt_BR.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Uma exceção ocorreu durante a autenticação. + + + Authentication credentials could not be found. + As credenciais de autenticação não foram encontradas. + + + Authentication request could not be processed due to a system problem. + A solicitação de autenticação não pôde ser processada devido a um problema no sistema. + + + Invalid credentials. + Credenciais inválidas. + + + Cookie has already been used by someone else. + Este cookie já foi usado por outra pessoa. + + + Not privileged to request the resource. + Sem privilégio para solicitar o recurso. + + + Invalid CSRF token. + Token CSRF inválido. + + + No authentication provider found to support the authentication token. + Nenhum provedor de autenticação encontrado para suportar o token de autenticação. + + + No session available, it either timed out or cookies are not enabled. + Nenhuma sessão disponível, ela expirou ou os cookies não estão habilitados. + + + No token could be found. + Nenhum token foi encontrado. + + + Username could not be found. + Nome de usuário não encontrado. + + + Account has expired. + A conta está expirada. + + + Credentials have expired. + As credenciais estão expiradas. + + + Account is disabled. + Conta desativada. + + + Account is locked. + A conta está travada. + + + Too many failed login attempts, please try again later. + Muitas tentativas de login malsucedidas, por favor, tente novamente mais tarde. + + + Invalid or expired login link. + Link de login inválido ou expirado. + + + Too many failed login attempts, please try again in %minutes% minute. + Muitas tentativas de login inválidas, por favor, tente novamente em um minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Muitas tentativas de login sem sucesso, por favor, tente novamente em %minutes% minutos. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.ro.xlf b/lib/symfony/security-core/Resources/translations/security.ro.xlf new file mode 100644 index 0000000000..3316275fde --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.ro.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + A apărut o eroare de autentificare. + + + Authentication credentials could not be found. + Informațiile de autentificare nu au fost găsite. + + + Authentication request could not be processed due to a system problem. + Sistemul nu a putut procesa cererea de autentificare din cauza unei erori. + + + Invalid credentials. + Date de autentificare invalide. + + + Cookie has already been used by someone else. + Cookie este folosit deja de altcineva. + + + Not privileged to request the resource. + Permisiuni insuficiente pentru resursa cerută. + + + Invalid CSRF token. + Token CSRF este invalid. + + + No authentication provider found to support the authentication token. + Nu a fost găsit nici un agent de autentificare pentru tokenul specificat. + + + No session available, it either timed out or cookies are not enabled. + Sesiunea nu mai este disponibilă, a expirat sau suportul pentru cookies nu este activat. + + + No token could be found. + Tokenul nu a putut fi găsit. + + + Username could not be found. + Numele de utilizator nu a fost găsit. + + + Account has expired. + Contul a expirat. + + + Credentials have expired. + Datele de autentificare au expirat. + + + Account is disabled. + Contul este dezactivat. + + + Account is locked. + Contul este blocat. + + + Too many failed login attempts, please try again later. + Prea multe încercări de autentificare eșuate, vă rugăm să încercați mai târziu. + + + Invalid or expired login link. + Link de autentificare invalid sau expirat. + + + Too many failed login attempts, please try again in %minutes% minute. + Prea multe încercări nereușite, încearcă din nou în %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Prea multe încercări eșuate de autentificare, vă rugăm să încercați din nou peste %minutes% minut.|Prea multe încercări eșuate de autentificare, vă rugăm să încercați din nou peste %minutes% minute. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.ru.xlf b/lib/symfony/security-core/Resources/translations/security.ru.xlf new file mode 100644 index 0000000000..8705a24cff --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.ru.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ошибка аутентификации. + + + Authentication credentials could not be found. + Аутентификационные данные не найдены. + + + Authentication request could not be processed due to a system problem. + Запрос аутентификации не может быть обработан в связи с проблемой в системе. + + + Invalid credentials. + Недействительные аутентификационные данные. + + + Cookie has already been used by someone else. + Cookie уже был использован кем-то другим. + + + Not privileged to request the resource. + Отсутствуют права на запрос этого ресурса. + + + Invalid CSRF token. + Недействительный токен CSRF. + + + No authentication provider found to support the authentication token. + Не найден провайдер аутентификации, поддерживающий токен аутентификации. + + + No session available, it either timed out or cookies are not enabled. + Сессия не найдена, ее время истекло, либо cookies не включены. + + + No token could be found. + Токен не найден. + + + Username could not be found. + Имя пользователя не найдено. + + + Account has expired. + Время действия учетной записи истекло. + + + Credentials have expired. + Время действия аутентификационных данных истекло. + + + Account is disabled. + Учетная запись отключена. + + + Account is locked. + Учетная запись заблокирована. + + + Too many failed login attempts, please try again later. + Слишком много неудачных попыток входа, пожалуйста, попробуйте позже. + + + Invalid or expired login link. + Ссылка для входа недействительна или просрочена. + + + Too many failed login attempts, please try again in %minutes% minute. + Слишком много неудачных попыток входа, повторите попытку через %minutes% минуту. + + + Too many failed login attempts, please try again in %minutes% minutes. + Слишком много неудачных попыток входа, повторите попытку через %minutes% минуту.|Слишком много неудачных попыток входа, повторите попытку через %minutes% минуты.|Слишком много неудачных попыток входа, повторите попытку через %minutes% минут. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.sk.xlf b/lib/symfony/security-core/Resources/translations/security.sk.xlf new file mode 100644 index 0000000000..3820bdccc7 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.sk.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Pri overovaní došlo k chybe. + + + Authentication credentials could not be found. + Overovacie údaje neboli nájdené. + + + Authentication request could not be processed due to a system problem. + Požiadavok na overenie nemohol byť spracovaný kvôli systémovej chybe. + + + Invalid credentials. + Neplatné prihlasovacie údaje. + + + Cookie has already been used by someone else. + Cookie už bolo použité niekým iným. + + + Not privileged to request the resource. + Nemáte oprávnenie pristupovať k prostriedku. + + + Invalid CSRF token. + Neplatný CSRF token. + + + No authentication provider found to support the authentication token. + Poskytovateľ pre overovací token nebol nájdený. + + + No session available, it either timed out or cookies are not enabled. + Session nie je k dispozíci, vypršala jej platnosť, alebo sú zakázané cookies. + + + No token could be found. + Token nebol nájdený. + + + Username could not be found. + Prihlasovacie meno nebolo nájdené. + + + Account has expired. + Platnosť účtu skončila. + + + Credentials have expired. + Platnosť prihlasovacích údajov skončila. + + + Account is disabled. + Účet je zakázaný. + + + Account is locked. + Účet je zablokovaný. + + + Too many failed login attempts, please try again later. + Príliš mnoho neúspešných pokusov o prihlásenie. Skúste to prosím znovu neskôr. + + + Invalid or expired login link. + Neplatný alebo expirovaný odkaz na prihlásenie. + + + Too many failed login attempts, please try again in %minutes% minute. + Príliš veľa neúspešných pokusov o prihlásenie. Skúste to znova o %minutes% minútu. + + + Too many failed login attempts, please try again in %minutes% minutes. + Príliš veľa neúspešných pokusov o prihlásenie, skúste to prosím znova o %minutes% minútu.|Príliš veľa neúspešných pokusov o prihlásenie, skúste to prosím znova o %minutes% minúty.|Príliš veľa neúspešných pokusov o prihlásenie, skúste to prosím znova o %minutes% minút. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.sl.xlf b/lib/symfony/security-core/Resources/translations/security.sl.xlf new file mode 100644 index 0000000000..2b7a592b79 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.sl.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Prišlo je do izjeme pri preverjanju avtentikacije. + + + Authentication credentials could not be found. + Poverilnic za avtentikacijo ni bilo mogoče najti. + + + Authentication request could not be processed due to a system problem. + Zahteve za avtentikacijo ni bilo mogoče izvesti zaradi sistemske težave. + + + Invalid credentials. + Neveljavne pravice. + + + Cookie has already been used by someone else. + Piškotek je uporabil že nekdo drug. + + + Not privileged to request the resource. + Nimate privilegijev za zahtevani vir. + + + Invalid CSRF token. + Neveljaven CSRF žeton. + + + No authentication provider found to support the authentication token. + Ponudnika avtentikacije za podporo prijavnega žetona ni bilo mogoče najti. + + + No session available, it either timed out or cookies are not enabled. + Seja ni na voljo, ali je potekla ali pa piškotki niso omogočeni. + + + No token could be found. + Žetona ni bilo mogoče najti. + + + Username could not be found. + Uporabniškega imena ni bilo mogoče najti. + + + Account has expired. + Račun je potekel. + + + Credentials have expired. + Poverilnice so potekle. + + + Account is disabled. + Račun je onemogočen. + + + Account is locked. + Račun je zaklenjen. + + + Too many failed login attempts, please try again later. + Preveč neuspelih poskusov prijave, poskusite znova pozneje. + + + Invalid or expired login link. + Neveljavna ali potekla povezava prijave. + + + Too many failed login attempts, please try again in %minutes% minute. + Preveč neuspelih poskusov prijave, poskusite znova čez %minutes% minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Preveč neuspešnih poskusov prijave, poskusite znova čez %minutes% minuto.|Preveč neuspešnih poskusov prijave, poskusite znova čez %minutes% minuti.|Preveč neuspešnih poskusov prijave, poskusite znova čez %minutes% minute.|Preveč neuspešnih poskusov prijave, poskusite znova čez %minutes% minut. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.sq.xlf b/lib/symfony/security-core/Resources/translations/security.sq.xlf new file mode 100644 index 0000000000..2ea888245e --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.sq.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ndodhi një problem në autentikim. + + + Authentication credentials could not be found. + Kredencialet e autentikimit nuk mund të gjendeshin. + + + Authentication request could not be processed due to a system problem. + Kërkesa për autentikim nuk mund të përpunohej për shkak të një problemi në sistem. + + + Invalid credentials. + Kredenciale të pavlefshme. + + + Cookie has already been used by someone else. + “Cookie” është përdorur tashmë nga dikush tjetër. + + + Not privileged to request the resource. + Nuk është i privilegjuar të kërkojë burimin. + + + Invalid CSRF token. + Identifikues i pavlefshëm CSRF. + + + No authentication provider found to support the authentication token. + Asnjë ofrues i vërtetimit nuk u gjet që të mbështesë simbolin e vërtetimit. + + + No session available, it either timed out or cookies are not enabled. + Nuk ka asnjë sesion të vlefshëm, i ka skaduar koha ose cookies nuk janë aktivizuar. + + + No token could be found. + Asnjë simbol identifikimi nuk mund të gjendej. + + + Username could not be found. + Emri i përdoruesit nuk mund të gjendej. + + + Account has expired. + Llogaria ka skaduar. + + + Credentials have expired. + Kredencialet kanë skaduar. + + + Account is disabled. + Llogaria është çaktivizuar. + + + Account is locked. + Llogaria është e kyçur. + + + Too many failed login attempts, please try again later. + Shumë përpjekje të dështuara autentikimi, provo përsëri më vonë. + + + Invalid or expired login link. + Link hyrje i pavlefshëm ose i skaduar. + + + Too many failed login attempts, please try again in %minutes% minute. + Shumë përpjekje të dështuara për identifikim; provo sërish pas %minutes% minutë. + + + Too many failed login attempts, please try again in %minutes% minutes. + Shumë përpjekje të dështuara për identifikim, ju lutemi provoni përsëri pas %minutes% minutash. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.sr_Cyrl.xlf b/lib/symfony/security-core/Resources/translations/security.sr_Cyrl.xlf new file mode 100644 index 0000000000..2192fe6e00 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.sr_Cyrl.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Изузетак при аутентификацији. + + + Authentication credentials could not be found. + Аутентификациони подаци нису пронађени. + + + Authentication request could not be processed due to a system problem. + Захтев за аутентификацију не може бити обрађен због системских проблема. + + + Invalid credentials. + Невалидни подаци за аутентификацију. + + + Cookie has already been used by someone else. + Колачић је већ искоришћен од стране неког другог. + + + Not privileged to request the resource. + Немате права приступа овом ресурсу. + + + Invalid CSRF token. + Невалидан CSRF токен. + + + No authentication provider found to support the authentication token. + Аутентификациони провајдер за подршку токена није пронађен. + + + No session available, it either timed out or cookies are not enabled. + Сесија није доступна, истекла је или су колачићи искључени. + + + No token could be found. + Токен не може бити пронађен. + + + Username could not be found. + Корисничко име не може бити пронађено. + + + Account has expired. + Налог је истекао. + + + Credentials have expired. + Подаци за аутентификацију су истекли. + + + Account is disabled. + Налог је онемогућен. + + + Account is locked. + Налог је закључан. + + + Too many failed login attempts, please try again later. + Превише неуспешних покушаја пријављивања, молим покушајте поново касније. + + + Invalid or expired login link. + Линк за пријављивање је истекао или је неисправан. + + + Too many failed login attempts, please try again in %minutes% minute. + Превише неуспешних покушаја пријављивања, молим покушајте поново за %minutes% минут. + + + Too many failed login attempts, please try again in %minutes% minutes. + Превише неуспешних покушаја пријављивања, покушајте поново за %minutes% минут.|Превише неуспешних покушаја пријављивања, покушајте поново за %minutes% минута. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.sr_Latn.xlf b/lib/symfony/security-core/Resources/translations/security.sr_Latn.xlf new file mode 100644 index 0000000000..6a925c5b0f --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.sr_Latn.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Izuzetak pri autentifikaciji. + + + Authentication credentials could not be found. + Autentifikacioni podaci nisu pronađeni. + + + Authentication request could not be processed due to a system problem. + Zahtev za autentifikaciju ne može biti obrađen zbog sistemskih problema. + + + Invalid credentials. + Nevalidni podaci za autentifikaciju. + + + Cookie has already been used by someone else. + Kolačić je već iskorišćen od strane nekog drugog. + + + Not privileged to request the resource. + Nemate prava pristupa ovom resursu. + + + Invalid CSRF token. + Nevalidan CSRF token. + + + No authentication provider found to support the authentication token. + Autentifikacioni provajder za podršku tokena nije pronađen. + + + No session available, it either timed out or cookies are not enabled. + Sesija nije dostupna, istekla je ili su kolačići isključeni. + + + No token could be found. + Token ne može biti pronađen. + + + Username could not be found. + Korisničko ime ne može biti pronađeno. + + + Account has expired. + Nalog je istekao. + + + Credentials have expired. + Podaci za autentifikaciju su istekli. + + + Account is disabled. + Nalog je onemogućen. + + + Account is locked. + Nalog je zaključan. + + + Too many failed login attempts, please try again later. + Previše neuspešnih pokušaja prijavljivanja, molim pokušajte ponovo kasnije. + + + Invalid or expired login link. + Link za prijavljivanje je istekao ili je neispravan. + + + Too many failed login attempts, please try again in %minutes% minute. + Previše neuspešnih pokušaja prijavljivanja, molim pokušajte ponovo za %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Previše neuspešnih pokušaja prijavljivanja, pokušajte ponovo za %minutes% minut.|Previše neuspešnih pokušaja prijavljivanja, pokušajte ponovo za %minutes% minuta. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.sv.xlf b/lib/symfony/security-core/Resources/translations/security.sv.xlf new file mode 100644 index 0000000000..dffe36df63 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.sv.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Ett autentiseringsfel har inträffat. + + + Authentication credentials could not be found. + Uppgifterna för autentisering kunde inte hittas. + + + Authentication request could not be processed due to a system problem. + Autentiseringen kunde inte genomföras på grund av systemfel. + + + Invalid credentials. + Felaktiga uppgifter. + + + Cookie has already been used by someone else. + Cookien har redan använts av någon annan. + + + Not privileged to request the resource. + Saknar rättigheter för resursen. + + + Invalid CSRF token. + Ogiltig CSRF-token. + + + No authentication provider found to support the authentication token. + Ingen leverantör för autentisering hittades för angiven autentiseringstoken. + + + No session available, it either timed out or cookies are not enabled. + Ingen session finns tillgänglig, antingen har den förfallit eller är cookies inte aktiverat. + + + No token could be found. + Ingen token kunde hittas. + + + Username could not be found. + Användarnamnet kunde inte hittas. + + + Account has expired. + Kontot har förfallit. + + + Credentials have expired. + Uppgifterna har förfallit. + + + Account is disabled. + Kontot är inaktiverat. + + + Account is locked. + Kontot är låst. + + + Too many failed login attempts, please try again later. + För många misslyckade inloggningsförsök, försök igen senare. + + + Invalid or expired login link. + Ogiltig eller utgången inloggningslänk. + + + Too many failed login attempts, please try again in %minutes% minute. + För många misslyckade inloggningsförsök, försök igen om %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + För många misslyckade inloggningsförsök, vänligen försök igen om %minutes% minuter. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.th.xlf b/lib/symfony/security-core/Resources/translations/security.th.xlf new file mode 100644 index 0000000000..0209b4c423 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.th.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + พบความผิดพลาดในการรับรองตัวตน + + + Authentication credentials could not be found. + ไม่พบข้อมูลในการรับรองตัวตน (credentials) + + + Authentication request could not be processed due to a system problem. + คำร้องในการรับรองตัวตนไม่สามารถดำเนินการได้ เนื่องมาจากปัญหาของระบบ + + + Invalid credentials. + ข้อมูลการรับรองตัวตนไม่ถูกต้อง + + + Cookie has already been used by someone else. + Cookie ถูกใช้งานไปแล้วด้วยผู้อื่น + + + Not privileged to request the resource. + ไม่ได้รับสิทธิ์ให้ใช้งานส่วนนี้ได้ + + + Invalid CSRF token. + CSRF token ไม่ถูกต้อง + + + No authentication provider found to support the authentication token. + ไม่พบ authentication provider ที่รองรับสำหรับ authentication token + + + No session available, it either timed out or cookies are not enabled. + ไม่มี session ที่พร้อมใช้งาน, Session หมดอายุไปแล้วหรือ cookies ไม่ถูกเปิดใช้งาน + + + No token could be found. + ไม่พบ token + + + Username could not be found. + ไม่พบ Username + + + Account has expired. + บัญชีหมดอายุไปแล้ว + + + Credentials have expired. + ข้อมูลการระบุตัวตนหมดอายุแล้ว + + + Account is disabled. + บัญชีถูกระงับแล้ว + + + Account is locked. + บัญชีถูกล็อกแล้ว + + + Too many failed login attempts, please try again later. + มีความพยายามเข้าสู่ระบบล้มเหลวมากเกินไป กรุณาลองใหม่ภายหลัง + + + Invalid or expired login link. + ลิงค์เข้าสู่ระบบไม่ถูกต้องหรือหมดอายุไปแล้ว + + + Too many failed login attempts, please try again in %minutes% minute. + มีความพยายามเข้าสู่ระบบล้มเหลวมากเกินไป โปรดลองอีกครั้งใน %minutes% นาที + + + Too many failed login attempts, please try again in %minutes% minutes. + มีความพยายามในการเข้าสู่ระบบล้มเหลวมากเกินไป โปรดลองอีกครั้งใน %minutes% นาที.|มีความพยายามในการเข้าสู่ระบบล้มเหลวมากเกินไป โปรดลองอีกครั้งใน %minutes% นาที. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.tl.xlf b/lib/symfony/security-core/Resources/translations/security.tl.xlf new file mode 100644 index 0000000000..aa47f179cd --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.tl.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Nagkaroon ng isang pagbubukod sa pagpapatotoo. + + + Authentication credentials could not be found. + Hindi matagpuan ang mga kredensyal ng pagpapatotoo. + + + Authentication request could not be processed due to a system problem. + Ang kahilingan sa pagpapatotoo ay hindi naproseso dahil sa isang problema sa system. + + + Invalid credentials. + Di-wastong mga kredensyal. + + + Cookie has already been used by someone else. + Ang Cookie ay ginamit na ng ibang tao. + + + Not privileged to request the resource. + Walang pribilehiyo upang humingi ng mga bagong mapagkukunan. + + + Invalid CSRF token. + Di-wastong token ng CSRF. + + + No authentication provider found to support the authentication token. + Walang nahanap na provider ng pagpapatotoo upang suportahan ang token ng pagpapatotoo. + + + No session available, it either timed out or cookies are not enabled. + Walang magagamit na session, alinman sa nag-time out o ang cookies ay hindi pinagana. + + + No token could be found. + Walang makitang token. + + + Username could not be found. + Hindi makita ang username. + + + Account has expired. + Nag-expire na ang account. + + + Credentials have expired. + Nag-expire na ang mga kredensyal. + + + Account is disabled. + Ang account ay hindi pinagana. + + + Account is locked. + Ang account ay naka-lock. + + + Too many failed login attempts, please try again later. + Napakaraming nabigong mga pagtatangka sa pag-login, mangyaring subukang muli sa ibang pagkakataon. + + + Invalid or expired login link. + Inbalido o nagexpire na ang link para makapaglogin. + + + Too many failed login attempts, please try again in %minutes% minute. + Napakaraming nabigong mga pagtatangka sa pag-login, pakisubukan ulit matapos ang %minutes% minuto. + + + Too many failed login attempts, please try again in %minutes% minutes. + Napakaraming nabigong mga pagtatangka sa pag-login, pakisubukan ulit matapos ang %minutes% minuto. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.tr.xlf b/lib/symfony/security-core/Resources/translations/security.tr.xlf new file mode 100644 index 0000000000..57b2b2a2c7 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.tr.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Bir yetkilendirme istisnası oluştu. + + + Authentication credentials could not be found. + Kimlik bilgileri bulunamadı. + + + Authentication request could not be processed due to a system problem. + Bir sistem hatası nedeniyle yetkilendirme isteği işleme alınamıyor. + + + Invalid credentials. + Geçersiz kimlik bilgileri. + + + Cookie has already been used by someone else. + Çerez bir başkası tarafından zaten kullanılmıştı. + + + Not privileged to request the resource. + Kaynak talebi için imtiyaz bulunamadı. + + + Invalid CSRF token. + Geçersiz CSRF fişi. + + + No authentication provider found to support the authentication token. + Yetkilendirme fişini destekleyecek yetkilendirme sağlayıcısı bulunamadı. + + + No session available, it either timed out or cookies are not enabled. + Oturum bulunamadı, zaman aşımına uğradı veya çerezler etkin değil. + + + No token could be found. + Fiş bulunamadı. + + + Username could not be found. + Kullanıcı adı bulunamadı. + + + Account has expired. + Hesap zaman aşımına uğradı. + + + Credentials have expired. + Kimlik bilgileri zaman aşımına uğradı. + + + Account is disabled. + Hesap engellenmiş. + + + Account is locked. + Hesap kilitlenmiş. + + + Too many failed login attempts, please try again later. + Çok fazla başarısız giriş denemesi, lütfen daha sonra tekrar deneyin. + + + Invalid or expired login link. + Geçersiz veya süresi dolmuş oturum açma bağlantısı. + + + Too many failed login attempts, please try again in %minutes% minute. + Çok fazla başarısız giriş denemesi, lütfen %minutes% dakika sonra tekrar deneyin. + + + Too many failed login attempts, please try again in %minutes% minutes. + Çok fazla başarısız giriş denemesi, lütfen %minutes% dakika sonra tekrar deneyin. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.uk.xlf b/lib/symfony/security-core/Resources/translations/security.uk.xlf new file mode 100644 index 0000000000..6b27de7cae --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.uk.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Помилка автентифікації. + + + Authentication credentials could not be found. + Автентифікаційні дані не знайдено. + + + Authentication request could not be processed due to a system problem. + Запит на автентифікацію не може бути опрацьовано у зв’язку з проблемою в системі. + + + Invalid credentials. + Невірні автентифікаційні дані. + + + Cookie has already been used by someone else. + Хтось інший вже використав цей сookie. + + + Not privileged to request the resource. + Відсутні права на запит цього ресурсу. + + + Invalid CSRF token. + Невірний токен CSRF. + + + No authentication provider found to support the authentication token. + Не знайдено провайдера автентифікації, що підтримує токен автентифікаціії. + + + No session available, it either timed out or cookies are not enabled. + Сесія недоступна, її час вийшов, або cookies вимкнено. + + + No token could be found. + Токен не знайдено. + + + Username could not be found. + Ім’я користувача не знайдено. + + + Account has expired. + Термін дії облікового запису вичерпано. + + + Credentials have expired. + Термін дії автентифікаційних даних вичерпано. + + + Account is disabled. + Обліковий запис відключено. + + + Account is locked. + Обліковий запис заблоковано. + + + Too many failed login attempts, please try again later. + Забагато невдалих спроб входу. Будь ласка, спробуйте пізніше. + + + Invalid or expired login link. + Посилання для входу недійсне, або термін його дії закінчився. + + + Too many failed login attempts, please try again in %minutes% minute. + Забагато невдалих спроб входу. Будь ласка, спробуйте знову через %minutes% хвилину. + + + Too many failed login attempts, please try again in %minutes% minutes. + Забагато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилину.|Забагато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилини.|Забагато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилин. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.ur.xlf b/lib/symfony/security-core/Resources/translations/security.ur.xlf new file mode 100644 index 0000000000..5c705cd0f7 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.ur.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + ایک تصدیقي خرابی پیش آگئی ۓ + + + Authentication credentials could not be found. + درج کردھ ریکارڈ نہیں مل سکا + + + Authentication request could not be processed due to a system problem. + سسٹم کی خرابی کی وجہ سے تصدیق کی درخواست پر کارروائی نہیں ہو سکی + + + Invalid credentials. + غلط ڈیٹا + + + Cookie has already been used by someone else. + کوکی پہلے ہی کسی اور کے ذریعہ استعمال ہو چکی ہے + + + Not privileged to request the resource. + وسائل کی درخواست کرنے کا اختیار نہیں ہے + + + Invalid CSRF token. + ٹوکن غلط ہے CSRF + + + No authentication provider found to support the authentication token. + تصدیقی ٹوکن کو سپورٹ کرنے کے لیے کوئی تصدیقی کنندہ نہیں ملا + + + No session available, it either timed out or cookies are not enabled. + کوئی سیشن دستیاب نہیں ہے، یا تو اس کا وقت ختم ہو گیا ہے یا کوکیز فعال نہیں ہیں + + + No token could be found. + کوئی ٹوکن نہیں مل سکا + + + Username could not be found. + يوذر نہیں مل سکا + + + Account has expired. + اکاؤنٹ کی میعاد ختم ہو گئی ہے + + + Credentials have expired. + اسناد کی میعاد ختم ہو چکی ہے + + + Account is disabled. + اکاؤنٹ بند کر دیا گیا ہے + + + Account is locked. + اکاؤنٹ لاک ہے + + + Too many failed login attempts, please try again later. + لاگ ان کی بہت زیادہ ناکام کوششیں ہو چکی ہیں، براۓ کرم بعد میں دوبارہ کوشش کریں + + + Invalid or expired login link. + غلط یا ختم شدھ لاگ ان لنک + + + Too many failed login attempts, please try again in %minutes% minute. + منٹ باد %minutes% لاگ ان کی بہت زیادہ ناکام کوششیں ہو چکی ہیں، براۓ کرم دوبارھ کوشيش کريں + + + Too many failed login attempts, please try again in %minutes% minutes. + بہت زیادہ ناکام لاگ ان کوششیں، براہ کرم %minutes% منٹ میں دوبارہ کوشش کریں۔ + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.uz.xlf b/lib/symfony/security-core/Resources/translations/security.uz.xlf new file mode 100644 index 0000000000..ec690c5f43 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.uz.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Autentifikatsiyada xatolik. + + + Authentication credentials could not be found. + Autentifikatsiya ma'lumotlari topilmadi. + + + Authentication request could not be processed due to a system problem. + Tizimdagi muammo tufayli autentifikatsiya so'rovi bajarilmadi. + + + Invalid credentials. + Noto'g'ri ma'lumot. + + + Cookie has already been used by someone else. + Cookie faylini allaqachon kimdir ishlatgan. + + + Not privileged to request the resource. + Sizda ushbu manbani talab qilishga ruxsat yo'q.. + + + Invalid CSRF token. + Noto'g'ri CSRF belgisi. + + + No authentication provider found to support the authentication token. + Haqiqiylikni tasdiqlovchi belgini qo'llab-quvvatlovchi biron bir autentifikatsiya provayderi topilmadi. + + + No session available, it either timed out or cookies are not enabled. + Sessiya topilmadi, muddati tugamadi yoki cookie-fayllar yoqilmagan. + + + No token could be found. + To'ken topilmadi. + + + Username could not be found. + Foydalanuvchi nomi topilmadi. + + + Account has expired. + Akkunt muddati tugagan. + + + Credentials have expired. + Autentifikatsiya ma'lumotlari muddati tugagan. + + + Account is disabled. + Akkunt o'chirilgan. + + + Account is locked. + Akkunt bloklangan. + + + Too many failed login attempts, please try again later. + Kirish urinishlari muvaffaqiyatsiz tugadi, keyinroq qayta urinib ko'ring. + + + Invalid or expired login link. + Kirish havolasi yaroqsiz yoki muddati tugagan. + + + Too many failed login attempts, please try again in %minutes% minute. + Kirish uchun muvaffaqiyatsiz urinishlar, %minutes% daqiqadan so'ng qayta urinib ko'ring. + + + Too many failed login attempts, please try again in %minutes% minutes. + Koʻplab muvaffaqiyatsiz kirish urinishlari, iltimos, %minutes% daqiqadan so'ng qayta urinib koʻring.|Koʻplab muvaffaqiyatsiz kirish urinishlari, iltimos, %minutes% daqiqadan so'ng qayta urinib koʻring. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.vi.xlf b/lib/symfony/security-core/Resources/translations/security.vi.xlf new file mode 100644 index 0000000000..fc4595c8d7 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.vi.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + Có lỗi trong quá trình xác thực. + + + Authentication credentials could not be found. + Thông tin dùng để xác thực không tìm thấy. + + + Authentication request could not be processed due to a system problem. + Yêu cầu xác thực không thể thực hiện do lỗi của hệ thống. + + + Invalid credentials. + Thông tin dùng để xác thực không hợp lệ. + + + Cookie has already been used by someone else. + Cookie đã được dùng bởi người dùng khác. + + + Not privileged to request the resource. + Không được phép yêu cầu tài nguyên. + + + Invalid CSRF token. + Mã CSRF không hợp lệ. + + + No authentication provider found to support the authentication token. + Không tìm thấy nhà cung cấp dịch vụ xác thực nào cho mã xác thực mà bạn sử dụng. + + + No session available, it either timed out or cookies are not enabled. + Không tìm thấy phiên làm việc. Phiên làm việc hoặc cookie có thể bị tắt. + + + No token could be found. + Không tìm thấy mã token. + + + Username could not be found. + Không tìm thấy tên người dùng. + + + Account has expired. + Tài khoản đã hết hạn. + + + Credentials have expired. + Thông tin xác thực đã hết hạn. + + + Account is disabled. + Tài khoản bị tạm ngừng. + + + Account is locked. + Tài khoản bị khóa. + + + Too many failed login attempts, please try again later. + Đăng nhập sai quá nhiều lần, vui lòng thử lại lần nữa. + + + Invalid or expired login link. + Liên kết đăng nhập không hợp lệ hoặc quá hạn. + + + Too many failed login attempts, please try again in %minutes% minute. + Quá nhiều lần thử đăng nhập không thành công, vui lòng thử lại sau %minutes% phút. + + + Too many failed login attempts, please try again in %minutes% minutes. + Quá nhiều lần đăng nhập không thành công, vui lòng thử lại sau %minutes% phút.|Quá nhiều lần đăng nhập không thành công, vui lòng thử lại sau %minutes% phút. + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.zh_CN.xlf b/lib/symfony/security-core/Resources/translations/security.zh_CN.xlf new file mode 100644 index 0000000000..01fe700953 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.zh_CN.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + 身份验证发生异常。 + + + Authentication credentials could not be found. + 没有找到身份验证的凭证。 + + + Authentication request could not be processed due to a system problem. + 由于系统故障,身份验证的请求无法被处理。 + + + Invalid credentials. + 无效的凭证。 + + + Cookie has already been used by someone else. + Cookie 已经被其他人使用。 + + + Not privileged to request the resource. + 没有权限请求此资源。 + + + Invalid CSRF token. + 无效的 CSRF token 。 + + + No authentication provider found to support the authentication token. + 没有找到支持此 token 的身份验证服务提供方。 + + + No session available, it either timed out or cookies are not enabled. + Session 不可用。会话超时或没有启用 cookies 。 + + + No token could be found. + 找不到 token 。 + + + Username could not be found. + 找不到用户名。 + + + Account has expired. + 帐号已过期。 + + + Credentials have expired. + 凭证已过期。 + + + Account is disabled. + 帐号已被禁用。 + + + Account is locked. + 帐号已被锁定。 + + + Too many failed login attempts, please try again later. + 登入失败的次数过多,请稍后再试。 + + + Invalid or expired login link. + 失效或过期的登入链接。 + + + Too many failed login attempts, please try again in %minutes% minute. + 登入失败的次数过多,请在%minutes%分钟后再试。 + + + Too many failed login attempts, please try again in %minutes% minutes. + 登录尝试失败次数过多,请在 %minutes% 分钟后重试。 + + + + diff --git a/lib/symfony/security-core/Resources/translations/security.zh_TW.xlf b/lib/symfony/security-core/Resources/translations/security.zh_TW.xlf new file mode 100644 index 0000000000..5368a35d59 --- /dev/null +++ b/lib/symfony/security-core/Resources/translations/security.zh_TW.xlf @@ -0,0 +1,83 @@ + + + + + + An authentication exception occurred. + 身份驗證發生異常。 + + + Authentication credentials could not be found. + 沒有找到身份驗證的憑證。 + + + Authentication request could not be processed due to a system problem. + 由於系統故障,身份驗證的請求無法被處理。 + + + Invalid credentials. + 登入憑證無效。 + + + Cookie has already been used by someone else. + Cookie 已經被其他人使用。 + + + Not privileged to request the resource. + 無權請求此資源。 + + + Invalid CSRF token. + 無效的 CSRF token。 + + + No authentication provider found to support the authentication token. + 找不到支援此 token 的身分驗證服務提供方。 + + + No session available, it either timed out or cookies are not enabled. + 沒有工作階段,可能是超過時間,或者是未啟用 Cookies。 + + + No token could be found. + 找不到 token。 + + + Username could not be found. + 找不到使用者名稱。 + + + Account has expired. + 帳號已經過期。 + + + Credentials have expired. + 憑證已經過期。 + + + Account is disabled. + 帳號已被停用。 + + + Account is locked. + 帳號已被鎖定。 + + + Too many failed login attempts, please try again later. + 登入失敗的次數過多,請稍後再試。 + + + Invalid or expired login link. + 登入連結無效或過期。 + + + Too many failed login attempts, please try again in %minutes% minute. + 登入失敗的次數過多,請 %minutes% 分鐘後再試。 + + + Too many failed login attempts, please try again in %minutes% minutes. + 登入嘗試次數過多,請 %minutes% 分鐘後再試。 + + + + diff --git a/lib/symfony/security-core/Role/Role.php b/lib/symfony/security-core/Role/Role.php new file mode 100644 index 0000000000..374eb59fe8 --- /dev/null +++ b/lib/symfony/security-core/Role/Role.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Role; + +/** + * Allows migrating session payloads from v4. + * + * @internal + */ +class Role +{ + private $role; + + private function __construct() + { + } + + public function __toString(): string + { + return $this->role; + } +} diff --git a/lib/symfony/security-core/Role/RoleHierarchy.php b/lib/symfony/security-core/Role/RoleHierarchy.php new file mode 100644 index 0000000000..da094d2bbf --- /dev/null +++ b/lib/symfony/security-core/Role/RoleHierarchy.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Role; + +/** + * RoleHierarchy defines a role hierarchy. + * + * @author Fabien Potencier + */ +class RoleHierarchy implements RoleHierarchyInterface +{ + private array $hierarchy; + /** @var array> */ + protected $map; + + /** + * @param array> $hierarchy + */ + public function __construct(array $hierarchy) + { + $this->hierarchy = $hierarchy; + + $this->buildRoleMap(); + } + + public function getReachableRoleNames(array $roles): array + { + $reachableRoles = $roles; + + foreach ($roles as $role) { + if (!isset($this->map[$role])) { + continue; + } + + foreach ($this->map[$role] as $r) { + $reachableRoles[] = $r; + } + } + + return array_values(array_unique($reachableRoles)); + } + + /** + * @return void + */ + protected function buildRoleMap() + { + $this->map = []; + foreach ($this->hierarchy as $main => $roles) { + $this->map[$main] = $roles; + $visited = []; + $additionalRoles = $roles; + while ($role = array_shift($additionalRoles)) { + if (!isset($this->hierarchy[$role])) { + continue; + } + + $visited[] = $role; + + foreach ($this->hierarchy[$role] as $roleToAdd) { + $this->map[$main][] = $roleToAdd; + } + + foreach (array_diff($this->hierarchy[$role], $visited) as $additionalRole) { + $additionalRoles[] = $additionalRole; + } + } + + $this->map[$main] = array_unique($this->map[$main]); + } + } +} diff --git a/lib/symfony/security-core/Role/RoleHierarchyInterface.php b/lib/symfony/security-core/Role/RoleHierarchyInterface.php new file mode 100644 index 0000000000..6e8fa81d07 --- /dev/null +++ b/lib/symfony/security-core/Role/RoleHierarchyInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Role; + +/** + * RoleHierarchyInterface is the interface for a role hierarchy. + * + * @author Fabien Potencier + */ +interface RoleHierarchyInterface +{ + /** + * @param string[] $roles + * + * @return string[] + */ + public function getReachableRoleNames(array $roles): array; +} diff --git a/lib/symfony/security-core/Role/SwitchUserRole.php b/lib/symfony/security-core/Role/SwitchUserRole.php new file mode 100644 index 0000000000..6a29fb4daa --- /dev/null +++ b/lib/symfony/security-core/Role/SwitchUserRole.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\Security\Core\Role; + +/** + * Allows migrating session payloads from v4. + * + * @internal + */ +class SwitchUserRole extends Role +{ + private $deprecationTriggered; + private $source; +} diff --git a/lib/symfony/security-core/Security.php b/lib/symfony/security-core/Security.php new file mode 100644 index 0000000000..bb2576a7ab --- /dev/null +++ b/lib/symfony/security-core/Security.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security as NewSecurityHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Helper class for commonly-needed security tasks. + * + * @deprecated since Symfony 6.2, use \Symfony\Bundle\SecurityBundle\Security instead + */ +class Security implements AuthorizationCheckerInterface +{ + public const ACCESS_DENIED_ERROR = '_security.403_error'; + public const AUTHENTICATION_ERROR = '_security.last_error'; + public const LAST_USERNAME = '_security.last_username'; + + /** + * @deprecated since Symfony 6.2, use \Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge::MAX_USERNAME_LENGTH instead + */ + public const MAX_USERNAME_LENGTH = 4096; + + private ContainerInterface $container; + + public function __construct(ContainerInterface $container, bool $triggerDeprecation = true) + { + $this->container = $container; + + if ($triggerDeprecation) { + trigger_deprecation('symfony/security-core', '6.2', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, NewSecurityHelper::class); + } + } + + public function getUser(): ?UserInterface + { + if (!$token = $this->getToken()) { + return null; + } + + return $token->getUser(); + } + + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied subject. + */ + public function isGranted(mixed $attributes, mixed $subject = null): bool + { + return $this->container->get('security.authorization_checker') + ->isGranted($attributes, $subject); + } + + public function getToken(): ?TokenInterface + { + return $this->container->get('security.token_storage')->getToken(); + } +} diff --git a/lib/symfony/security-core/Signature/Exception/ExpiredSignatureException.php b/lib/symfony/security-core/Signature/Exception/ExpiredSignatureException.php new file mode 100644 index 0000000000..8986c62f3d --- /dev/null +++ b/lib/symfony/security-core/Signature/Exception/ExpiredSignatureException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Wouter de Jong + */ +class ExpiredSignatureException extends RuntimeException +{ +} diff --git a/lib/symfony/security-core/Signature/Exception/InvalidSignatureException.php b/lib/symfony/security-core/Signature/Exception/InvalidSignatureException.php new file mode 100644 index 0000000000..72102fe86c --- /dev/null +++ b/lib/symfony/security-core/Signature/Exception/InvalidSignatureException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Wouter de Jong + */ +class InvalidSignatureException extends RuntimeException +{ +} diff --git a/lib/symfony/security-core/Signature/ExpiredSignatureStorage.php b/lib/symfony/security-core/Signature/ExpiredSignatureStorage.php new file mode 100644 index 0000000000..20803b9742 --- /dev/null +++ b/lib/symfony/security-core/Signature/ExpiredSignatureStorage.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * @author Ryan Weaver + */ +final class ExpiredSignatureStorage +{ + private CacheItemPoolInterface $cache; + private int $lifetime; + + public function __construct(CacheItemPoolInterface $cache, int $lifetime) + { + $this->cache = $cache; + $this->lifetime = $lifetime; + } + + public function countUsages(string $hash): int + { + $key = rawurlencode($hash); + if (!$this->cache->hasItem($key)) { + return 0; + } + + return $this->cache->getItem($key)->get(); + } + + public function incrementUsages(string $hash): void + { + $item = $this->cache->getItem(rawurlencode($hash)); + + if (!$item->isHit()) { + $item->expiresAfter($this->lifetime); + } + + $item->set($this->countUsages($hash) + 1); + $this->cache->save($item); + } +} diff --git a/lib/symfony/security-core/Signature/SignatureHasher.php b/lib/symfony/security-core/Signature/SignatureHasher.php new file mode 100644 index 0000000000..15f6803dd1 --- /dev/null +++ b/lib/symfony/security-core/Signature/SignatureHasher.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature; + +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; +use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException; +use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Creates and validates secure hashes used in login links and remember-me cookies. + * + * @author Wouter de Jong + * @author Ryan Weaver + */ +class SignatureHasher +{ + private PropertyAccessorInterface $propertyAccessor; + private array $signatureProperties; + private string $secret; + private ?ExpiredSignatureStorage $expiredSignaturesStorage; + private ?int $maxUses; + + /** + * @param array $signatureProperties Properties of the User; the hash is invalidated if these properties change + * @param ExpiredSignatureStorage|null $expiredSignaturesStorage If provided, secures a sequence of hashes that are expired + * @param int|null $maxUses Used together with $expiredSignatureStorage to allow a maximum usage of a hash + */ + public function __construct(PropertyAccessorInterface $propertyAccessor, array $signatureProperties, #[\SensitiveParameter] string $secret, ?ExpiredSignatureStorage $expiredSignaturesStorage = null, ?int $maxUses = null) + { + if (!$secret) { + throw new InvalidArgumentException('A non-empty secret is required.'); + } + + $this->propertyAccessor = $propertyAccessor; + $this->signatureProperties = $signatureProperties; + $this->secret = $secret; + $this->expiredSignaturesStorage = $expiredSignaturesStorage; + $this->maxUses = $maxUses; + } + + /** + * Verifies the hash using the provided user identifier and expire time. + * + * This method must be called before the user object is loaded from a provider. + * + * @param int $expires The expiry time as a unix timestamp + * @param string $hash The plaintext hash provided by the request + * + * @throws InvalidSignatureException If the signature does not match the provided parameters + * @throws ExpiredSignatureException If the signature is no longer valid + */ + public function acceptSignatureHash(string $userIdentifier, int $expires, string $hash): void + { + if ($expires < time()) { + throw new ExpiredSignatureException('Signature has expired.'); + } + $hmac = substr($hash, 0, 44); + $payload = substr($hash, 44).':'.$expires.':'.$userIdentifier; + + if (!hash_equals($hmac, $this->generateHash($payload))) { + throw new InvalidSignatureException('Invalid or expired signature.'); + } + } + + /** + * Verifies the hash using the provided user and expire time. + * + * @param int $expires The expiry time as a unix timestamp + * @param string $hash The plaintext hash provided by the request + * + * @throws InvalidSignatureException If the signature does not match the provided parameters + * @throws ExpiredSignatureException If the signature is no longer valid + */ + public function verifySignatureHash(UserInterface $user, int $expires, string $hash): void + { + if ($expires < time()) { + throw new ExpiredSignatureException('Signature has expired.'); + } + + if (!hash_equals($hash, $this->computeSignatureHash($user, $expires))) { + throw new InvalidSignatureException('Invalid or expired signature.'); + } + + if ($this->expiredSignaturesStorage && $this->maxUses) { + if ($this->expiredSignaturesStorage->countUsages($hash) >= $this->maxUses) { + throw new ExpiredSignatureException(\sprintf('Signature can only be used "%d" times.', $this->maxUses)); + } + + $this->expiredSignaturesStorage->incrementUsages($hash); + } + } + + /** + * Computes the secure hash for the provided user and expire time. + * + * @param int $expires The expiry time as a unix timestamp + */ + public function computeSignatureHash(UserInterface $user, int $expires): string + { + $userIdentifier = $user->getUserIdentifier(); + $fieldsHash = hash_init('sha256'); + + foreach ($this->signatureProperties as $property) { + $value = $this->propertyAccessor->getValue($user, $property) ?? ''; + if ($value instanceof \DateTimeInterface) { + $value = $value->format('c'); + } + + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new \InvalidArgumentException(\sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, $user::class, get_debug_type($value))); + } + hash_update($fieldsHash, ':'.base64_encode($value)); + } + + $fieldsHash = strtr(base64_encode(hash_final($fieldsHash, true)), '+/=', '-_~'); + + return $this->generateHash($fieldsHash.':'.$expires.':'.$userIdentifier).$fieldsHash; + } + + private function generateHash(string $tokenValue): string + { + return strtr(base64_encode(hash_hmac('sha256', $tokenValue, $this->secret, true)), '+/=', '-_~'); + } +} diff --git a/lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php b/lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php new file mode 100644 index 0000000000..bf2a2b9a15 --- /dev/null +++ b/lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Test; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + +/** + * Abstract test case for access decision strategies. + * + * @author Alexander M. Turek + */ +abstract class AccessDecisionStrategyTestCase extends TestCase +{ + /** + * @dataProvider provideStrategyTests + * + * @param VoterInterface[] $voters + */ + final public function testDecide(AccessDecisionStrategyInterface $strategy, array $voters, bool $expected) + { + $token = $this->createMock(TokenInterface::class); + $manager = new AccessDecisionManager($voters, $strategy); + + $this->assertSame($expected, $manager->decide($token, ['ROLE_FOO'])); + } + + /** + * @return iterable + */ + abstract public static function provideStrategyTests(): iterable; + + /** + * @return VoterInterface[] + */ + final protected static function getVoters(int $grants, int $denies, int $abstains): array + { + $voters = []; + for ($i = 0; $i < $grants; ++$i) { + $voters[] = static::getVoter(VoterInterface::ACCESS_GRANTED); + } + for ($i = 0; $i < $denies; ++$i) { + $voters[] = static::getVoter(VoterInterface::ACCESS_DENIED); + } + for ($i = 0; $i < $abstains; ++$i) { + $voters[] = static::getVoter(VoterInterface::ACCESS_ABSTAIN); + } + + return $voters; + } + + final protected static function getVoter(int $vote): VoterInterface + { + return new class($vote) implements VoterInterface { + private int $vote; + + public function __construct(int $vote) + { + $this->vote = $vote; + } + + public function vote(TokenInterface $token, $subject, array $attributes): int + { + return $this->vote; + } + }; + } +} diff --git a/lib/symfony/security-core/User/AttributesBasedUserProviderInterface.php b/lib/symfony/security-core/User/AttributesBasedUserProviderInterface.php new file mode 100644 index 0000000000..9d79422aa4 --- /dev/null +++ b/lib/symfony/security-core/User/AttributesBasedUserProviderInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\UserNotFoundException; + +/** + * Overrides UserProviderInterface to add an "attributes" argument on loadUserByIdentifier. + * This is particularly useful with self-contained access tokens. + * + * @template-covariant TUser of UserInterface + * + * @template-extends UserProviderInterface + */ +interface AttributesBasedUserProviderInterface extends UserProviderInterface +{ + /** + * Loads the user for the given user identifier (e.g. username or email) and attributes. + * + * This method must throw UserNotFoundException if the user is not found. + * + * @return TUser + * + * @throws UserNotFoundException + */ + public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface; +} diff --git a/lib/symfony/security-core/User/ChainUserChecker.php b/lib/symfony/security-core/User/ChainUserChecker.php new file mode 100644 index 0000000000..f889d35d55 --- /dev/null +++ b/lib/symfony/security-core/User/ChainUserChecker.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +final class ChainUserChecker implements UserCheckerInterface +{ + /** + * @param iterable $checkers + */ + public function __construct(private readonly iterable $checkers) + { + } + + public function checkPreAuth(UserInterface $user): void + { + foreach ($this->checkers as $checker) { + $checker->checkPreAuth($user); + } + } + + public function checkPostAuth(UserInterface $user): void + { + foreach ($this->checkers as $checker) { + $checker->checkPostAuth($user); + } + } +} diff --git a/lib/symfony/security-core/User/ChainUserProvider.php b/lib/symfony/security-core/User/ChainUserProvider.php new file mode 100644 index 0000000000..c1964aa8cc --- /dev/null +++ b/lib/symfony/security-core/User/ChainUserProvider.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; + +/** + * Chain User Provider. + * + * This provider calls several leaf providers in a chain until one is able to + * handle the request. + * + * @author Johannes M. Schmitt + * + * @template-implements UserProviderInterface + */ +class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterface +{ + private iterable $providers; + + /** + * @param iterable $providers + */ + public function __construct(iterable $providers) + { + $this->providers = $providers; + } + + /** + * @return UserProviderInterface[] + */ + public function getProviders(): array + { + if ($this->providers instanceof \Traversable) { + return iterator_to_array($this->providers); + } + + return $this->providers; + } + + /** + * @internal for compatibility with Symfony 5.4 + */ + public function loadUserByUsername(string $username): UserInterface + { + return $this->loadUserByIdentifier($username); + } + + /** + * @param array $attributes + */ + public function loadUserByIdentifier(string $identifier/* , array $attributes = [] */): UserInterface + { + $attributes = \func_num_args() > 1 ? func_get_arg(1) : []; + foreach ($this->providers as $provider) { + try { + if ($provider instanceof AttributesBasedUserProviderInterface || $provider instanceof self) { + return $provider->loadUserByIdentifier($identifier, $attributes); + } + + return $provider->loadUserByIdentifier($identifier); + } catch (UserNotFoundException) { + // try next one + } + } + + $ex = new UserNotFoundException(\sprintf('There is no user with identifier "%s".', $identifier)); + $ex->setUserIdentifier($identifier); + throw $ex; + } + + public function refreshUser(UserInterface $user): UserInterface + { + $supportedUserFound = false; + + foreach ($this->providers as $provider) { + try { + if (!$provider->supportsClass(get_debug_type($user))) { + continue; + } + + return $provider->refreshUser($user); + } catch (UnsupportedUserException) { + // try next one + } catch (UserNotFoundException) { + $supportedUserFound = true; + // try next one + } + } + + if ($supportedUserFound) { + $username = $user->getUserIdentifier(); + $e = new UserNotFoundException(\sprintf('There is no user with name "%s".', $username)); + $e->setUserIdentifier($username); + throw $e; + } else { + throw new UnsupportedUserException(\sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', get_debug_type($user))); + } + } + + public function supportsClass(string $class): bool + { + foreach ($this->providers as $provider) { + if ($provider->supportsClass($class)) { + return true; + } + } + + return false; + } + + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + foreach ($this->providers as $provider) { + if ($provider instanceof PasswordUpgraderInterface) { + try { + $provider->upgradePassword($user, $newHashedPassword); + } catch (UnsupportedUserException) { + // ignore: password upgrades are opportunistic + } + } + } + } +} diff --git a/lib/symfony/security-core/User/EquatableInterface.php b/lib/symfony/security-core/User/EquatableInterface.php new file mode 100644 index 0000000000..3fa9e48884 --- /dev/null +++ b/lib/symfony/security-core/User/EquatableInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +/** + * EquatableInterface used to test if two objects are equal in security + * and re-authentication context. + * + * @author Dariusz Górecki + */ +interface EquatableInterface +{ + /** + * The equality comparison should neither be done by referential equality + * nor by comparing identities (i.e. getId() === getId()). + * + * However, you do not need to compare every attribute, but only those that + * are relevant for assessing whether re-authentication is required. + */ + public function isEqualTo(UserInterface $user): bool; +} diff --git a/lib/symfony/security-core/User/InMemoryUser.php b/lib/symfony/security-core/User/InMemoryUser.php new file mode 100644 index 0000000000..c319e1f937 --- /dev/null +++ b/lib/symfony/security-core/User/InMemoryUser.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\Security\Core\User; + +/** + * UserInterface implementation used by the in-memory user provider. + * + * This should not be used for anything else. + * + * @author Robin Chalas + * @author Fabien Potencier + */ +final class InMemoryUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface, \Stringable +{ + private string $username; + private ?string $password; + private bool $enabled; + private array $roles; + + public function __construct(?string $username, ?string $password, array $roles = [], bool $enabled = true) + { + if ('' === $username || null === $username) { + throw new \InvalidArgumentException('The username cannot be empty.'); + } + + $this->username = $username; + $this->password = $password; + $this->enabled = $enabled; + $this->roles = $roles; + } + + public function __toString(): string + { + return $this->getUserIdentifier(); + } + + public function getRoles(): array + { + return $this->roles; + } + + public function getPassword(): ?string + { + return $this->password; + } + + /** + * Returns the identifier for this user (e.g. its username or email address). + */ + public function getUserIdentifier(): string + { + return $this->username; + } + + /** + * Checks whether the user is enabled. + * + * Internally, if this method returns false, the authentication system + * will throw a DisabledException and prevent login. + * + * @return bool true if the user is enabled, false otherwise + * + * @see DisabledException + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + public function eraseCredentials(): void + { + } + + public function isEqualTo(UserInterface $user): bool + { + if (!$user instanceof self) { + return false; + } + + if ($this->getPassword() !== $user->getPassword()) { + return false; + } + + $currentRoles = array_map('strval', (array) $this->getRoles()); + $newRoles = array_map('strval', (array) $user->getRoles()); + $rolesChanged = \count($currentRoles) !== \count($newRoles) || \count($currentRoles) !== \count(array_intersect($currentRoles, $newRoles)); + if ($rolesChanged) { + return false; + } + + if ($this->getUserIdentifier() !== $user->getUserIdentifier()) { + return false; + } + + if ($this->isEnabled() !== $user->isEnabled()) { + return false; + } + + return true; + } +} diff --git a/lib/symfony/security-core/User/InMemoryUserChecker.php b/lib/symfony/security-core/User/InMemoryUserChecker.php new file mode 100644 index 0000000000..a493b00e79 --- /dev/null +++ b/lib/symfony/security-core/User/InMemoryUserChecker.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\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\DisabledException; + +/** + * Checks the state of the in-memory user account. + * + * @author Fabien Potencier + */ +class InMemoryUserChecker implements UserCheckerInterface +{ + /** + * @return void + */ + public function checkPreAuth(UserInterface $user) + { + if (!$user instanceof InMemoryUser) { + return; + } + + if (!$user->isEnabled()) { + $ex = new DisabledException('User account is disabled.'); + $ex->setUser($user); + throw $ex; + } + } + + /** + * @return void + */ + public function checkPostAuth(UserInterface $user) + { + } +} diff --git a/lib/symfony/security-core/User/InMemoryUserProvider.php b/lib/symfony/security-core/User/InMemoryUserProvider.php new file mode 100644 index 0000000000..5fb05d0f6f --- /dev/null +++ b/lib/symfony/security-core/User/InMemoryUserProvider.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; + +/** + * InMemoryUserProvider is a simple non persistent user provider. + * + * Useful for testing, demonstration, prototyping, and for simple needs + * (a backend with a unique admin for instance) + * + * @author Fabien Potencier + * + * @template-implements UserProviderInterface + */ +class InMemoryUserProvider implements UserProviderInterface +{ + /** + * @var array + */ + private array $users = []; + + /** + * The user array is a hash where the keys are usernames and the values are + * an array of attributes: 'password', 'enabled', and 'roles'. + * + * @param array}> $users An array of users + */ + public function __construct(array $users = []) + { + foreach ($users as $username => $attributes) { + $password = $attributes['password'] ?? null; + $enabled = $attributes['enabled'] ?? true; + $roles = $attributes['roles'] ?? []; + $user = new InMemoryUser($username, $password, $roles, $enabled); + + $this->createUser($user); + } + } + + /** + * Adds a new User to the provider. + * + * @return void + * + * @throws \LogicException + */ + public function createUser(UserInterface $user) + { + if (!$user instanceof InMemoryUser) { + trigger_deprecation('symfony/security-core', '6.3', 'Passing users that are not instance of "%s" to "%s" is deprecated, "%s" given.', InMemoryUser::class, __METHOD__, get_debug_type($user)); + } + + $userIdentifier = strtolower($user->getUserIdentifier()); + if (isset($this->users[$userIdentifier])) { + throw new \LogicException('Another user with the same username already exists.'); + } + + $this->users[$userIdentifier] = $user; + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + $user = $this->getUser($identifier); + + return new InMemoryUser($user->getUserIdentifier(), $user->getPassword(), $user->getRoles(), $user->isEnabled()); + } + + public function refreshUser(UserInterface $user): UserInterface + { + if (!$user instanceof InMemoryUser) { + throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user))); + } + + $storedUser = $this->getUser($user->getUserIdentifier()); + $userIdentifier = $storedUser->getUserIdentifier(); + + return new InMemoryUser($userIdentifier, $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); + } + + public function supportsClass(string $class): bool + { + return InMemoryUser::class == $class; + } + + /** + * Returns the user by given username. + * + * @return InMemoryUser change return type on 7.0 + * + * @throws UserNotFoundException if user whose given username does not exist + */ + private function getUser(string $username): UserInterface + { + if (!isset($this->users[strtolower($username)])) { + $ex = new UserNotFoundException(\sprintf('Username "%s" does not exist.', $username)); + $ex->setUserIdentifier($username); + + throw $ex; + } + + return $this->users[strtolower($username)]; + } +} diff --git a/lib/symfony/security-core/User/LegacyPasswordAuthenticatedUserInterface.php b/lib/symfony/security-core/User/LegacyPasswordAuthenticatedUserInterface.php new file mode 100644 index 0000000000..fcffe0b91b --- /dev/null +++ b/lib/symfony/security-core/User/LegacyPasswordAuthenticatedUserInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +/** + * For users that can be authenticated using a password/salt couple. + * + * Once all password hashes have been upgraded to a modern algorithm via password migrations, + * implement {@see PasswordAuthenticatedUserInterface} instead. + * + * @author Robin Chalas + */ +interface LegacyPasswordAuthenticatedUserInterface extends PasswordAuthenticatedUserInterface +{ + /** + * Returns the salt that was originally used to hash the password. + */ + public function getSalt(): ?string; +} diff --git a/lib/symfony/security-core/User/MissingUserProvider.php b/lib/symfony/security-core/User/MissingUserProvider.php new file mode 100644 index 0000000000..9869259a5c --- /dev/null +++ b/lib/symfony/security-core/User/MissingUserProvider.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + +/** + * MissingUserProvider is a dummy user provider used to throw proper exception + * when a firewall requires a user provider but none was defined. + * + * @internal + * + * @template-implements UserProviderInterface + */ +class MissingUserProvider implements UserProviderInterface +{ + /** + * @param string $firewall the firewall missing a provider + */ + public function __construct(string $firewall) + { + throw new InvalidConfigurationException(\sprintf('"%s" firewall requires a user provider but none was defined.', $firewall)); + } + + public function loadUserByUsername(string $username): UserInterface + { + throw new \BadMethodCallException(); + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + throw new \BadMethodCallException(); + } + + public function refreshUser(UserInterface $user): UserInterface + { + throw new \BadMethodCallException(); + } + + public function supportsClass(string $class): bool + { + throw new \BadMethodCallException(); + } +} diff --git a/lib/symfony/security-core/User/OidcUser.php b/lib/symfony/security-core/User/OidcUser.php new file mode 100644 index 0000000000..bcce363fa1 --- /dev/null +++ b/lib/symfony/security-core/User/OidcUser.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +/** + * UserInterface implementation used by the access-token security workflow with an OIDC server. + */ +class OidcUser implements UserInterface +{ + private array $additionalClaims = []; + + public function __construct( + private ?string $userIdentifier = null, + private array $roles = ['ROLE_USER'], + + // Standard Claims (https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) + private ?string $sub = null, + private ?string $name = null, + private ?string $givenName = null, + private ?string $familyName = null, + private ?string $middleName = null, + private ?string $nickname = null, + private ?string $preferredUsername = null, + private ?string $profile = null, + private ?string $picture = null, + private ?string $website = null, + private ?string $email = null, + private ?bool $emailVerified = null, + private ?string $gender = null, + private ?string $birthdate = null, + private ?string $zoneinfo = null, + private ?string $locale = null, + private ?string $phoneNumber = null, + private ?bool $phoneNumberVerified = null, + private ?array $address = null, + private ?\DateTimeInterface $updatedAt = null, + + // Additional Claims (https://openid.net/specs/openid-connect-core-1_0.html#AdditionalClaims) + ...$additionalClaims, + ) { + if (null === $sub || '' === $sub) { + throw new \InvalidArgumentException('The "sub" claim cannot be empty.'); + } + + $this->additionalClaims = $additionalClaims['additionalClaims'] ?? $additionalClaims; + } + + /** + * OIDC or OAuth specs don't have any "role" notion. + * + * If you want to implement "roles" from your OIDC server, + * send a "roles" constructor argument to this object + * (e.g.: using a custom UserProvider). + */ + public function getRoles(): array + { + return $this->roles; + } + + public function getUserIdentifier(): string + { + return (string) ($this->userIdentifier ?? $this->getSub()); + } + + public function eraseCredentials(): void + { + } + + public function getSub(): ?string + { + return $this->sub; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getGivenName(): ?string + { + return $this->givenName; + } + + public function getFamilyName(): ?string + { + return $this->familyName; + } + + public function getMiddleName(): ?string + { + return $this->middleName; + } + + public function getNickname(): ?string + { + return $this->nickname; + } + + public function getPreferredUsername(): ?string + { + return $this->preferredUsername; + } + + public function getProfile(): ?string + { + return $this->profile; + } + + public function getPicture(): ?string + { + return $this->picture; + } + + public function getWebsite(): ?string + { + return $this->website; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function getEmailVerified(): ?bool + { + return $this->emailVerified; + } + + public function getGender(): ?string + { + return $this->gender; + } + + public function getBirthdate(): ?string + { + return $this->birthdate; + } + + public function getZoneinfo(): ?string + { + return $this->zoneinfo; + } + + public function getLocale(): ?string + { + return $this->locale; + } + + public function getPhoneNumber(): ?string + { + return $this->phoneNumber; + } + + public function getphoneNumberVerified(): ?bool + { + return $this->phoneNumberVerified; + } + + public function getAddress(): ?array + { + return $this->address; + } + + public function getUpdatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public function getAdditionalClaims(): array + { + return $this->additionalClaims; + } +} diff --git a/lib/symfony/security-core/User/PasswordAuthenticatedUserInterface.php b/lib/symfony/security-core/User/PasswordAuthenticatedUserInterface.php new file mode 100644 index 0000000000..478c9e38f9 --- /dev/null +++ b/lib/symfony/security-core/User/PasswordAuthenticatedUserInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +/** + * For users that can be authenticated using a password. + * + * @author Robin Chalas + * @author Wouter de Jong + */ +interface PasswordAuthenticatedUserInterface +{ + /** + * Returns the hashed password used to authenticate the user. + * + * Usually on authentication, a plain-text password will be compared to this value. + */ + public function getPassword(): ?string; +} diff --git a/lib/symfony/security-core/User/PasswordUpgraderInterface.php b/lib/symfony/security-core/User/PasswordUpgraderInterface.php new file mode 100644 index 0000000000..fd21f14a81 --- /dev/null +++ b/lib/symfony/security-core/User/PasswordUpgraderInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +/** + * @author Nicolas Grekas + */ +interface PasswordUpgraderInterface +{ + /** + * Upgrades the hashed password of a user, typically for using a better hash algorithm. + * + * This method should persist the new password in the user storage and update the $user object accordingly. + * Because you don't want your users not being able to log in, this method should be opportunistic: + * it's fine if it does nothing or if it fails without throwing any exception. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void; +} diff --git a/lib/symfony/security-core/User/UserCheckerInterface.php b/lib/symfony/security-core/User/UserCheckerInterface.php new file mode 100644 index 0000000000..91f21c71d0 --- /dev/null +++ b/lib/symfony/security-core/User/UserCheckerInterface.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\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\AccountStatusException; + +/** + * Implement to throw AccountStatusException during the authentication process. + * + * Can be used when you want to check the account status, e.g when the account is + * disabled or blocked. This should not be used to make authentication decisions. + * + * @author Fabien Potencier + */ +interface UserCheckerInterface +{ + /** + * Checks the user account before authentication. + * + * @return void + * + * @throws AccountStatusException + */ + public function checkPreAuth(UserInterface $user); + + /** + * Checks the user account after authentication. + * + * @return void + * + * @throws AccountStatusException + */ + public function checkPostAuth(UserInterface $user); +} diff --git a/lib/symfony/security-core/User/UserInterface.php b/lib/symfony/security-core/User/UserInterface.php new file mode 100644 index 0000000000..a543c35cae --- /dev/null +++ b/lib/symfony/security-core/User/UserInterface.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +/** + * Represents the interface that all user classes must implement. + * + * This interface is useful because the authentication layer can deal with + * the object through its lifecycle, assigning roles and so on. + * + * Regardless of how your users are loaded or where they come from (a database, + * configuration, web service, etc.), you will have a class that implements + * this interface. Objects that implement this interface are created and + * loaded by different objects that implement UserProviderInterface. + * + * @see UserProviderInterface + * + * @author Fabien Potencier + */ +interface UserInterface +{ + /** + * Returns the roles granted to the user. + * + * public function getRoles() + * { + * return ['ROLE_USER']; + * } + * + * Alternatively, the roles might be stored in a ``roles`` property, + * and populated in any number of different ways when the user object + * is created. + * + * @return string[] + */ + public function getRoles(): array; + + /** + * Removes sensitive data from the user. + * + * This is important if, at any given point, sensitive information like + * the plain-text password is stored on this object. + * + * @return void + */ + public function eraseCredentials(); + + /** + * Returns the identifier for this user (e.g. username or email address). + */ + public function getUserIdentifier(): string; +} diff --git a/lib/symfony/security-core/User/UserProviderInterface.php b/lib/symfony/security-core/User/UserProviderInterface.php new file mode 100644 index 0000000000..0a4d562d61 --- /dev/null +++ b/lib/symfony/security-core/User/UserProviderInterface.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; + +/** + * Represents a class that loads UserInterface objects from some source for the authentication system. + * + * In a typical authentication configuration, a user identifier (e.g. a + * username or email address) credential enters the system (via form login, or + * any method). The user provider that is configured with that authentication + * method is asked to load the UserInterface object for the given identifier (via + * loadUserByIdentifier) so that the rest of the process can continue. + * + * Internally, a user provider can load users from any source (databases, + * configuration, web service). This is totally independent of how the authentication + * information is submitted or what the UserInterface object looks like. + * + * @author Fabien Potencier + * + * @template-covariant TUser of UserInterface + */ +interface UserProviderInterface +{ + /** + * Refreshes the user. + * + * It is up to the implementation to decide if the user data should be + * totally reloaded (e.g. from the database), or if the UserInterface + * object can just be merged into some internal array of users / identity + * map. + * + * @return UserInterface + * + * @psalm-return TUser + * + * @throws UnsupportedUserException if the user is not supported + * @throws UserNotFoundException if the user is not found + */ + public function refreshUser(UserInterface $user); + + /** + * Whether this provider supports the given user class. + * + * @return bool + */ + public function supportsClass(string $class); + + /** + * Loads the user for the given user identifier (e.g. username or email). + * + * This method must throw UserNotFoundException if the user is not found. + * + * @return TUser + * + * @throws UserNotFoundException + */ + public function loadUserByIdentifier(string $identifier): UserInterface; +} diff --git a/lib/symfony/security-core/Validator/Constraints/UserPassword.php b/lib/symfony/security-core/Validator/Constraints/UserPassword.php new file mode 100644 index 0000000000..e258d5866a --- /dev/null +++ b/lib/symfony/security-core/Validator/Constraints/UserPassword.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * @Annotation + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class UserPassword extends Constraint +{ + public const INVALID_PASSWORD_ERROR = '2d2a8bb4-ddc8-45e4-9b0f-8670d3a3e290'; + + protected const ERROR_NAMES = [ + self::INVALID_PASSWORD_ERROR => 'INVALID_PASSWORD_ERROR', + ]; + + public $message = 'This value should be the user\'s current password.'; + public $service = 'security.validator.user_password'; + + public function __construct(?array $options = null, ?string $message = null, ?string $service = null, ?array $groups = null, mixed $payload = null) + { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->service = $service ?? $this->service; + } + + public function validatedBy(): string + { + return $this->service; + } +} diff --git a/lib/symfony/security-core/Validator/Constraints/UserPasswordValidator.php b/lib/symfony/security-core/Validator/Constraints/UserPasswordValidator.php new file mode 100644 index 0000000000..3f549874e2 --- /dev/null +++ b/lib/symfony/security-core/Validator/Constraints/UserPasswordValidator.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Validator\Constraints; + +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +class UserPasswordValidator extends ConstraintValidator +{ + private TokenStorageInterface $tokenStorage; + private PasswordHasherFactoryInterface $hasherFactory; + + public function __construct(TokenStorageInterface $tokenStorage, PasswordHasherFactoryInterface $hasherFactory) + { + $this->tokenStorage = $tokenStorage; + $this->hasherFactory = $hasherFactory; + } + + /** + * @return void + */ + public function validate(mixed $password, Constraint $constraint) + { + if (!$constraint instanceof UserPassword) { + throw new UnexpectedTypeException($constraint, UserPassword::class); + } + + if (null === $password || '' === $password) { + $this->context->buildViolation($constraint->message) + ->setCode(UserPassword::INVALID_PASSWORD_ERROR) + ->addViolation(); + + return; + } + + if (!\is_string($password)) { + throw new UnexpectedTypeException($password, 'string'); + } + + $user = $this->tokenStorage->getToken()->getUser(); + + if (!$user instanceof PasswordAuthenticatedUserInterface) { + throw new ConstraintDefinitionException(\sprintf('The "%s" class must implement the "%s" interface.', get_debug_type($user), PasswordAuthenticatedUserInterface::class)); + } + + $hasher = $this->hasherFactory->getPasswordHasher($user); + + if (null === $user->getPassword() || !$hasher->verify($user->getPassword(), $password, $user instanceof LegacyPasswordAuthenticatedUserInterface ? $user->getSalt() : null)) { + $this->context->buildViolation($constraint->message) + ->setCode(UserPassword::INVALID_PASSWORD_ERROR) + ->addViolation(); + } + } +} diff --git a/lib/symfony/security-core/composer.json b/lib/symfony/security-core/composer.json new file mode 100644 index 0000000000..54a7e3921a --- /dev/null +++ b/lib/symfony/security-core/composer.json @@ -0,0 +1,53 @@ +{ + "name": "symfony/security-core", + "type": "library", + "description": "Symfony Security Component - Core Library", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "psr/container": "^1.1|^2.0", + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/ldap": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<5.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Security\\Core\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/lib/symfony/security-csrf/CHANGELOG.md b/lib/symfony/security-csrf/CHANGELOG.md new file mode 100644 index 0000000000..1476c99b76 --- /dev/null +++ b/lib/symfony/security-csrf/CHANGELOG.md @@ -0,0 +1,13 @@ +CHANGELOG +========= + +6.0 +--- + + * Remove the `SessionInterface $session` constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead + * Using `SessionTokenStorage` outside a request context throws a `SessionNotFoundException` + +5.3 +--- + +The CHANGELOG for version 5.3 and earlier can be found at https://github.com/symfony/symfony/blob/5.3/src/Symfony/Component/Security/CHANGELOG.md diff --git a/lib/symfony/security-csrf/CsrfToken.php b/lib/symfony/security-csrf/CsrfToken.php new file mode 100644 index 0000000000..57f972e620 --- /dev/null +++ b/lib/symfony/security-csrf/CsrfToken.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf; + +/** + * A CSRF token. + * + * @author Bernhard Schussek + */ +class CsrfToken +{ + private string $id; + private string $value; + + public function __construct(string $id, #[\SensitiveParameter] ?string $value) + { + $this->id = $id; + $this->value = $value ?? ''; + } + + /** + * Returns the ID of the CSRF token. + */ + public function getId(): string + { + return $this->id; + } + + /** + * Returns the value of the CSRF token. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Returns the value of the CSRF token. + */ + public function __toString(): string + { + return $this->value; + } +} diff --git a/lib/symfony/security-csrf/CsrfTokenManager.php b/lib/symfony/security-csrf/CsrfTokenManager.php new file mode 100644 index 0000000000..94b03589f8 --- /dev/null +++ b/lib/symfony/security-csrf/CsrfTokenManager.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; + +/** + * Default implementation of {@link CsrfTokenManagerInterface}. + * + * @author Bernhard Schussek + * @author Kévin Dunglas + */ +class CsrfTokenManager implements CsrfTokenManagerInterface +{ + private TokenGeneratorInterface $generator; + private TokenStorageInterface $storage; + private \Closure|string $namespace; + + /** + * @param $namespace + * * null: generates a namespace using $_SERVER['HTTPS'] + * * string: uses the given string + * * RequestStack: generates a namespace using the current main request + * * callable: uses the result of this callable (must return a string) + */ + public function __construct(?TokenGeneratorInterface $generator = null, ?TokenStorageInterface $storage = null, string|RequestStack|callable|null $namespace = null) + { + $this->generator = $generator ?? new UriSafeTokenGenerator(); + $this->storage = $storage ?? new NativeSessionTokenStorage(); + + $superGlobalNamespaceGenerator = fn () => !empty($_SERVER['HTTPS']) && 'off' !== strtolower($_SERVER['HTTPS']) ? 'https-' : ''; + + if (null === $namespace) { + $this->namespace = $superGlobalNamespaceGenerator; + } elseif ($namespace instanceof RequestStack) { + $this->namespace = function () use ($namespace, $superGlobalNamespaceGenerator) { + if ($request = $namespace->getMainRequest()) { + return $request->isSecure() ? 'https-' : ''; + } + + return $superGlobalNamespaceGenerator(); + }; + } elseif ($namespace instanceof \Closure || \is_string($namespace)) { + $this->namespace = $namespace; + } elseif (\is_callable($namespace)) { + $this->namespace = $namespace(...); + } else { + throw new InvalidArgumentException(\sprintf('$namespace must be a string, a callable returning a string, null or an instance of "RequestStack". "%s" given.', get_debug_type($namespace))); + } + } + + public function getToken(string $tokenId): CsrfToken + { + $namespacedId = $this->getNamespace().$tokenId; + if ($this->storage->hasToken($namespacedId)) { + $value = $this->storage->getToken($namespacedId); + } else { + $value = $this->generator->generateToken(); + + $this->storage->setToken($namespacedId, $value); + } + + return new CsrfToken($tokenId, $this->randomize($value)); + } + + public function refreshToken(string $tokenId): CsrfToken + { + $namespacedId = $this->getNamespace().$tokenId; + $value = $this->generator->generateToken(); + + $this->storage->setToken($namespacedId, $value); + + return new CsrfToken($tokenId, $this->randomize($value)); + } + + public function removeToken(string $tokenId): ?string + { + return $this->storage->removeToken($this->getNamespace().$tokenId); + } + + public function isTokenValid(CsrfToken $token): bool + { + $namespacedId = $this->getNamespace().$token->getId(); + if (!$this->storage->hasToken($namespacedId)) { + return false; + } + + return hash_equals($this->storage->getToken($namespacedId), $this->derandomize($token->getValue())); + } + + private function getNamespace(): string + { + return \is_callable($ns = $this->namespace) ? $ns() : $ns; + } + + private function randomize(string $value): string + { + $key = random_bytes(32); + $value = $this->xor($value, $key); + + return \sprintf('%s.%s.%s', substr(hash('xxh128', $key), 0, 1 + (\ord($key[0]) % 32)), rtrim(strtr(base64_encode($key), '+/', '-_'), '='), rtrim(strtr(base64_encode($value), '+/', '-_'), '=')); + } + + private function derandomize(string $value): string + { + $parts = explode('.', $value); + if (3 !== \count($parts)) { + return $value; + } + $key = base64_decode(strtr($parts[1], '-_', '+/')); + if ('' === $key || false === $key) { + return $value; + } + $value = base64_decode(strtr($parts[2], '-_', '+/')); + + return $this->xor($value, $key); + } + + private function xor(string $value, string $key): string + { + if (\strlen($value) > \strlen($key)) { + $key = str_repeat($key, ceil(\strlen($value) / \strlen($key))); + } + + return $value ^ $key; + } +} diff --git a/lib/symfony/security-csrf/CsrfTokenManagerInterface.php b/lib/symfony/security-csrf/CsrfTokenManagerInterface.php new file mode 100644 index 0000000000..14984a9312 --- /dev/null +++ b/lib/symfony/security-csrf/CsrfTokenManagerInterface.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\Security\Csrf; + +/** + * Manages CSRF tokens. + * + * @author Bernhard Schussek + */ +interface CsrfTokenManagerInterface +{ + /** + * Returns a CSRF token for the given ID. + * + * If previously no token existed for the given ID, a new token is + * generated. Otherwise the existing token is returned (with the same value, + * not the same instance). + * + * @param string $tokenId The token ID. You may choose an arbitrary value + * for the ID + */ + public function getToken(string $tokenId): CsrfToken; + + /** + * Generates a new token value for the given ID. + * + * This method will generate a new token for the given token ID, independent + * of whether a token value previously existed or not. It can be used to + * enforce once-only tokens in environments with high security needs. + * + * @param string $tokenId The token ID. You may choose an arbitrary value + * for the ID + */ + public function refreshToken(string $tokenId): CsrfToken; + + /** + * Invalidates the CSRF token with the given ID, if one exists. + * + * @return string|null Returns the removed token value if one existed, NULL + * otherwise + */ + public function removeToken(string $tokenId): ?string; + + /** + * Returns whether the given CSRF token is valid. + */ + public function isTokenValid(CsrfToken $token): bool; +} diff --git a/lib/symfony/security-csrf/Exception/TokenNotFoundException.php b/lib/symfony/security-csrf/Exception/TokenNotFoundException.php new file mode 100644 index 0000000000..936afdeb11 --- /dev/null +++ b/lib/symfony/security-csrf/Exception/TokenNotFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Bernhard Schussek + */ +class TokenNotFoundException extends RuntimeException +{ +} diff --git a/lib/symfony/security-csrf/LICENSE b/lib/symfony/security-csrf/LICENSE new file mode 100644 index 0000000000..0138f8f071 --- /dev/null +++ b/lib/symfony/security-csrf/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/symfony/security-csrf/README.md b/lib/symfony/security-csrf/README.md new file mode 100644 index 0000000000..90b7bfe5ea --- /dev/null +++ b/lib/symfony/security-csrf/README.md @@ -0,0 +1,29 @@ +Security Component - CSRF +========================= + +The Security CSRF (cross-site request forgery) component provides a class +`CsrfTokenManager` for generating and validating CSRF tokens. + +Sponsor +------- + +The Security component for Symfony 6.4 is [backed][1] by [SymfonyCasts][2]. + +Learn Symfony faster by watching real projects being built and actively coding +along with them. SymfonyCasts bridges that learning gap, bringing you video +tutorials and coding challenges. Code on! + +Help Symfony by [sponsoring][3] its development! + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/security.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) + +[1]: https://symfony.com/backers +[2]: https://symfonycasts.com +[3]: https://symfony.com/sponsor diff --git a/lib/symfony/security-csrf/TokenGenerator/TokenGeneratorInterface.php b/lib/symfony/security-csrf/TokenGenerator/TokenGeneratorInterface.php new file mode 100644 index 0000000000..9874092e94 --- /dev/null +++ b/lib/symfony/security-csrf/TokenGenerator/TokenGeneratorInterface.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\Security\Csrf\TokenGenerator; + +/** + * Generates CSRF tokens. + * + * @author Bernhard Schussek + */ +interface TokenGeneratorInterface +{ + /** + * Generates a CSRF token. + */ + public function generateToken(): string; +} diff --git a/lib/symfony/security-csrf/TokenGenerator/UriSafeTokenGenerator.php b/lib/symfony/security-csrf/TokenGenerator/UriSafeTokenGenerator.php new file mode 100644 index 0000000000..a31594408f --- /dev/null +++ b/lib/symfony/security-csrf/TokenGenerator/UriSafeTokenGenerator.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\TokenGenerator; + +/** + * Generates CSRF tokens. + * + * @author Bernhard Schussek + */ +class UriSafeTokenGenerator implements TokenGeneratorInterface +{ + private int $entropy; + + /** + * Generates URI-safe CSRF tokens. + * + * @param int $entropy The amount of entropy collected for each token (in bits) + */ + public function __construct(int $entropy = 256) + { + if ($entropy <= 7) { + throw new \InvalidArgumentException('Entropy should be greater than 7.'); + } + + $this->entropy = $entropy; + } + + public function generateToken(): string + { + // Generate an URI safe base64 encoded string that does not contain "+", + // "/" or "=" which need to be URL encoded and make URLs unnecessarily + // longer. + $bytes = random_bytes(intdiv($this->entropy, 8)); + + return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); + } +} diff --git a/lib/symfony/security-csrf/TokenStorage/ClearableTokenStorageInterface.php b/lib/symfony/security-csrf/TokenStorage/ClearableTokenStorageInterface.php new file mode 100644 index 0000000000..185c4a7e33 --- /dev/null +++ b/lib/symfony/security-csrf/TokenStorage/ClearableTokenStorageInterface.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\Security\Csrf\TokenStorage; + +/** + * @author Christian Flothmann + */ +interface ClearableTokenStorageInterface extends TokenStorageInterface +{ + /** + * Removes all CSRF tokens. + * + * @return void + */ + public function clear(); +} diff --git a/lib/symfony/security-csrf/TokenStorage/NativeSessionTokenStorage.php b/lib/symfony/security-csrf/TokenStorage/NativeSessionTokenStorage.php new file mode 100644 index 0000000000..7de8b52969 --- /dev/null +++ b/lib/symfony/security-csrf/TokenStorage/NativeSessionTokenStorage.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\TokenStorage; + +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; + +/** + * Token storage that uses PHP's native session handling. + * + * @author Bernhard Schussek + */ +class NativeSessionTokenStorage implements ClearableTokenStorageInterface +{ + /** + * The namespace used to store values in the session. + */ + public const SESSION_NAMESPACE = '_csrf'; + + private bool $sessionStarted = false; + private string $namespace; + + /** + * Initializes the storage with a session namespace. + * + * @param string $namespace The namespace under which the token is stored in the session + */ + public function __construct(string $namespace = self::SESSION_NAMESPACE) + { + $this->namespace = $namespace; + } + + public function getToken(string $tokenId): string + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + if (!isset($_SESSION[$this->namespace][$tokenId])) { + throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); + } + + return (string) $_SESSION[$this->namespace][$tokenId]; + } + + /** + * @return void + */ + public function setToken(string $tokenId, #[\SensitiveParameter] string $token) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + $_SESSION[$this->namespace][$tokenId] = $token; + } + + public function hasToken(string $tokenId): bool + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + return isset($_SESSION[$this->namespace][$tokenId]); + } + + public function removeToken(string $tokenId): ?string + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + if (!isset($_SESSION[$this->namespace][$tokenId])) { + return null; + } + + $token = (string) $_SESSION[$this->namespace][$tokenId]; + + unset($_SESSION[$this->namespace][$tokenId]); + + if (!$_SESSION[$this->namespace]) { + unset($_SESSION[$this->namespace]); + } + + return $token; + } + + /** + * @return void + */ + public function clear() + { + unset($_SESSION[$this->namespace]); + } + + private function startSession(): void + { + if (\PHP_SESSION_NONE === session_status()) { + session_start(); + } + + $this->sessionStarted = true; + } +} diff --git a/lib/symfony/security-csrf/TokenStorage/SessionTokenStorage.php b/lib/symfony/security-csrf/TokenStorage/SessionTokenStorage.php new file mode 100644 index 0000000000..4b3c3e56a5 --- /dev/null +++ b/lib/symfony/security-csrf/TokenStorage/SessionTokenStorage.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\TokenStorage; + +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; + +/** + * Token storage that uses a Symfony Session object. + * + * @author Bernhard Schussek + */ +class SessionTokenStorage implements ClearableTokenStorageInterface +{ + /** + * The namespace used to store values in the session. + */ + public const SESSION_NAMESPACE = '_csrf'; + + private RequestStack $requestStack; + private string $namespace; + + /** + * Initializes the storage with a RequestStack object and a session namespace. + * + * @param string $namespace The namespace under which the token is stored in the requestStack + */ + public function __construct(RequestStack $requestStack, string $namespace = self::SESSION_NAMESPACE) + { + $this->requestStack = $requestStack; + $this->namespace = $namespace; + } + + public function getToken(string $tokenId): string + { + $session = $this->getSession(); + if (!$session->isStarted()) { + $session->start(); + } + + if (!$session->has($this->namespace.'/'.$tokenId)) { + throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); + } + + return (string) $session->get($this->namespace.'/'.$tokenId); + } + + /** + * @return void + */ + public function setToken(string $tokenId, #[\SensitiveParameter] string $token) + { + $session = $this->getSession(); + if (!$session->isStarted()) { + $session->start(); + } + + $session->set($this->namespace.'/'.$tokenId, $token); + } + + public function hasToken(string $tokenId): bool + { + $session = $this->getSession(); + if (!$session->isStarted()) { + $session->start(); + } + + return $session->has($this->namespace.'/'.$tokenId); + } + + public function removeToken(string $tokenId): ?string + { + $session = $this->getSession(); + if (!$session->isStarted()) { + $session->start(); + } + + return $session->remove($this->namespace.'/'.$tokenId); + } + + /** + * @return void + */ + public function clear() + { + $session = $this->getSession(); + foreach (array_keys($session->all()) as $key) { + if (str_starts_with($key, $this->namespace.'/')) { + $session->remove($key); + } + } + } + + /** + * @throws SessionNotFoundException + */ + private function getSession(): SessionInterface + { + return $this->requestStack->getSession(); + } +} diff --git a/lib/symfony/security-csrf/TokenStorage/TokenStorageInterface.php b/lib/symfony/security-csrf/TokenStorage/TokenStorageInterface.php new file mode 100644 index 0000000000..32c71921bd --- /dev/null +++ b/lib/symfony/security-csrf/TokenStorage/TokenStorageInterface.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\TokenStorage; + +/** + * Stores CSRF tokens. + * + * @author Bernhard Schussek + */ +interface TokenStorageInterface +{ + /** + * Reads a stored CSRF token. + * + * @throws \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException If the token ID does not exist + */ + public function getToken(string $tokenId): string; + + /** + * Stores a CSRF token. + * + * @return void + */ + public function setToken(string $tokenId, #[\SensitiveParameter] string $token); + + /** + * Removes a CSRF token. + * + * @return string|null Returns the removed token if one existed, NULL + * otherwise + */ + public function removeToken(string $tokenId): ?string; + + /** + * Checks whether a token with the given token ID exists. + */ + public function hasToken(string $tokenId): bool; +} diff --git a/lib/symfony/security-csrf/composer.json b/lib/symfony/security-csrf/composer.json new file mode 100644 index 0000000000..30bba30d28 --- /dev/null +++ b/lib/symfony/security-csrf/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/security-csrf", + "type": "library", + "description": "Symfony Security Component - CSRF Library", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/http-foundation": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Security\\Csrf\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index 21a1423735..84a0567698 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -38,6 +38,7 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Csrf\CsrfExtension; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryBuilderInterface; @@ -45,6 +46,7 @@ use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\Forms; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Error\Error; use Twig\Error\SyntaxError; use Twig\RuntimeLoader\FactoryRuntimeLoader; @@ -98,6 +100,9 @@ abstract class Controller extends AbstractController /** @var FormFactoryBuilderInterface Factory form builder (from Symfony form component @link https://symfony.com/doc/current/components/form.html) */ private FormFactoryBuilderInterface $oFormFactoryBuilder; + /** @var CsrfTokenManager Csrf manager (from Symfony form component @link https://symfony.com/doc/current/security/csrf.html) */ + private CsrfTokenManager $oCsrfTokenManager; + /** * Controller constructor. * @@ -113,6 +118,22 @@ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditional $this->m_aDefaultParams = []; $this->m_aBlockParams = []; $this->SetModuleName($sModuleName); + + // Initialize Symfony components + $this->InitSymfonyComponents($sViewPath, $sModuleName); + } + + /** + * Init Symfony components. + * + * @param string $sViewPath + * @param string $sModuleName + * + * @return void + */ + private function InitSymfonyComponents(string $sViewPath, string $sModuleName): void + { + // Twig environment $aAdditionalPaths[] = APPROOT.'lib/symfony/twig-bridge/Resources/views/Form'; $aAdditionalPaths[] = APPROOT.'templates'; if (strlen($sViewPath) > 0) { @@ -127,22 +148,16 @@ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditional } } - // Initialize Symfony components - $this->InitSymfonyComponents();; - } - - /** - * Init controllers vars related to Symfony components. - * - * @return void - */ - private function InitSymfonyComponents(): void - { - // a request object representation from PHP request globals + // PHP Request object representation from PHP request globals $this->oRequest = Request::createFromGlobals(); - // initialize the form factory builder to handle Request objects - $this->oFormFactoryBuilder = Forms::createFormFactoryBuilder()->addExtension(new HttpFoundationExtension()); + // Initialize the CSRF token manager + $this->oCsrfTokenManager = new CsrfTokenManager(); + + // Initialize the form factory builder to handle Request objects + $this->oFormFactoryBuilder = Forms::createFormFactoryBuilder() + ->addExtension(new HttpFoundationExtension()) + ->addExtension(new CsrfExtension($this->oCsrfTokenManager)); } /** @@ -171,14 +186,14 @@ public function InitFromModule() public function SetViewPath($sViewPath, $aAdditionalPaths = []) { $oTwig = TwigHelper::GetTwigEnvironment($sViewPath, $aAdditionalPaths); - $formEngine = new TwigRendererEngine(['application/forms/itop_console_layout.twig'], $oTwig); + /** @link https://github.com/symfony/twig-bridge/blob/6.4/CHANGELOG.md#320 */ + $formEngine = new TwigRendererEngine(['application/forms/itop_console_layout.html.twig'], $oTwig); $oTwig->addRuntimeLoader(new FactoryRuntimeLoader([ FormRenderer::class => function () use ($formEngine): FormRenderer { - return new FormRenderer($formEngine, null); + return new FormRenderer($formEngine, $this->oCsrfTokenManager); }, ])); - $oExt = new FormExtension(); - $oTwig->addExtension($oExt); + $oTwig->addExtension(new FormExtension()); $this->m_oTwig = $oTwig; } diff --git a/sources/Application/TwigBase/Twig/Extension.php b/sources/Application/TwigBase/Twig/Extension.php index b1dfc68601..09ddb718bf 100644 --- a/sources/Application/TwigBase/Twig/Extension.php +++ b/sources/Application/TwigBase/Twig/Extension.php @@ -59,11 +59,6 @@ public static function GetFilters() return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); }); - // Alias of dict_s, to be compatible with Symfony/Twig standard - $aFilters[] = new TwigFilter('trans', function ($sStringCode, $aData = null, $sTransDomain = false) { - return Dict::S($sStringCode); - }); - // Filter to format a string via the Dict::Format function // Usage in twig: {{ 'String:ToTranslate'|dict_format() }} $aFilters[] = new TwigFilter('dict_format', function ($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null) { diff --git a/sources/Application/TwigBase/Twig/TwigHelper.php b/sources/Application/TwigBase/Twig/TwigHelper.php index 18eeef0752..fd460a321e 100644 --- a/sources/Application/TwigBase/Twig/TwigHelper.php +++ b/sources/Application/TwigBase/Twig/TwigHelper.php @@ -11,12 +11,14 @@ use Combodo\iTop\Application\UI\Base\Component\Html\Html; use Combodo\iTop\Application\UI\Base\UIBlock; use Combodo\iTop\Application\WebPage\WebPage; +use Combodo\iTop\Forms\Twig\Extension\FormCompatibilityExtension; use Combodo\iTop\Renderer\BlockRenderer; use CoreTemplateException; use ExecutionKPI; use IssueLog; use Twig\Environment; use Twig\Error\Error; +use Twig\Extension\DebugExtension; use Twig\Loader\FilesystemLoader; use utils; @@ -80,7 +82,10 @@ public static function GetTwigEnvironment($sViewPath, $aAdditionalPaths = array( $oLoader->addPath($sAdditionalPath); } - $oTwig = new Environment($oLoader); + // Create Twig environment + $oTwig = new Environment($oLoader, [ + 'debug' => utils::IsDevelopmentEnvironment(), + ]); Extension::RegisterTwigExtensions($oTwig); if (!utils::IsDevelopmentEnvironment()) { // Disable the cache in development environment @@ -90,7 +95,9 @@ public static function GetTwigEnvironment($sViewPath, $aAdditionalPaths = array( $oTwig->setCache($sCachePath); } + $oTwig->addExtension(new DebugExtension()); $oTwig->addExtension(new UIBlockExtension()); + $oTwig->addExtension(new FormCompatibilityExtension()); return $oTwig; } diff --git a/sources/Forms/Twig/Extension/FormCompatibilityExtension.php b/sources/Forms/Twig/Extension/FormCompatibilityExtension.php new file mode 100644 index 0000000000..ce6217401b --- /dev/null +++ b/sources/Forms/Twig/Extension/FormCompatibilityExtension.php @@ -0,0 +1,47 @@ + Date: Fri, 10 Oct 2025 10:38:01 +0200 Subject: [PATCH 10/10] =?UTF-8?q?N=C2=B08771=20-=20Add=20Symfony=20form=20?= =?UTF-8?q?component=20to=20iTop=20core=20-=20add=20symfony=20security=20t?= =?UTF-8?q?est=20folder=20to=20denied?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/composer/installed.php | 4 +- .../Test/AccessDecisionStrategyTestCase.php | 80 ------------------- .../Dependencies/Composer/iTopComposer.php | 1 + 3 files changed, 3 insertions(+), 82 deletions(-) delete mode 100644 lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 803afc5db1..e7f922b19b 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '29f05fefe6f5d4f4ca70729400221fc7b1cc0303', + 'reference' => '6183a9d3f3accabe858d865d1050861c1b0d57ef', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '29f05fefe6f5d4f4ca70729400221fc7b1cc0303', + 'reference' => '6183a9d3f3accabe858d865d1050861c1b0d57ef', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php b/lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php deleted file mode 100644 index bf2a2b9a15..0000000000 --- a/lib/symfony/security-core/Test/AccessDecisionStrategyTestCase.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Test; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; -use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface; -use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; - -/** - * Abstract test case for access decision strategies. - * - * @author Alexander M. Turek - */ -abstract class AccessDecisionStrategyTestCase extends TestCase -{ - /** - * @dataProvider provideStrategyTests - * - * @param VoterInterface[] $voters - */ - final public function testDecide(AccessDecisionStrategyInterface $strategy, array $voters, bool $expected) - { - $token = $this->createMock(TokenInterface::class); - $manager = new AccessDecisionManager($voters, $strategy); - - $this->assertSame($expected, $manager->decide($token, ['ROLE_FOO'])); - } - - /** - * @return iterable - */ - abstract public static function provideStrategyTests(): iterable; - - /** - * @return VoterInterface[] - */ - final protected static function getVoters(int $grants, int $denies, int $abstains): array - { - $voters = []; - for ($i = 0; $i < $grants; ++$i) { - $voters[] = static::getVoter(VoterInterface::ACCESS_GRANTED); - } - for ($i = 0; $i < $denies; ++$i) { - $voters[] = static::getVoter(VoterInterface::ACCESS_DENIED); - } - for ($i = 0; $i < $abstains; ++$i) { - $voters[] = static::getVoter(VoterInterface::ACCESS_ABSTAIN); - } - - return $voters; - } - - final protected static function getVoter(int $vote): VoterInterface - { - return new class($vote) implements VoterInterface { - private int $vote; - - public function __construct(int $vote) - { - $this->vote = $vote; - } - - public function vote(TokenInterface $token, $subject, array $attributes): int - { - return $this->vote; - } - }; - } -} diff --git a/sources/Dependencies/Composer/iTopComposer.php b/sources/Dependencies/Composer/iTopComposer.php index 17deb0570d..3aae0bf039 100644 --- a/sources/Dependencies/Composer/iTopComposer.php +++ b/sources/Dependencies/Composer/iTopComposer.php @@ -92,6 +92,7 @@ public function ListDeniedFilesRelPaths(): array 'symfony/mailer/Test', 'symfony/mime/Test', 'symfony/routing/Tests', + 'symfony/security-core/Test', 'symfony/stopwatch/Tests', 'symfony/translation-contracts/Test', 'symfony/twig-bridge/Test',